@fitchmultz/pi-subagents 0.52.18 → 0.52.19

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +4 -0
  3. package/dist/agent/host.js +12 -0
  4. package/dist/agent/index.js +70 -56
  5. package/dist/agent/launch.js +12 -3
  6. package/dist/agent/notifications.js +5 -5
  7. package/dist/agent/parking.js +26 -15
  8. package/dist/agent/runner.js +18 -16
  9. package/dist/agent/tool-schemas.js +2 -0
  10. package/dist/agent/usage.js +42 -3
  11. package/dist/agent/view.js +13 -13
  12. package/dist/agent/wake.js +17 -15
  13. package/dist/build-info.js +1 -1
  14. package/dist/pi-intercom/broker/broker.js +2 -2
  15. package/dist/pi-intercom/broker/snapshots.d.ts +25 -7
  16. package/dist/pi-intercom/broker/snapshots.js +56 -25
  17. package/dist/pi-intercom/broker/spawn-claim.js +89 -0
  18. package/dist/pi-intercom/broker/spawn.js +31 -78
  19. package/dist/pi-intercom/lifecycle.js +26 -26
  20. package/dist/shared/post-exit-stdio-guard.js +44 -37
  21. package/docs/development.md +25 -0
  22. package/docs/intercom.md +13 -0
  23. package/docs/reference.md +16 -5
  24. package/package.json +1 -1
  25. package/scripts/build.mjs +41 -14
  26. package/skills/pi-subagents/SKILL.md +8 -4
  27. package/src/agent/contracts.ts +17 -0
  28. package/src/agent/host.ts +19 -0
  29. package/src/agent/index.ts +91 -62
  30. package/src/agent/launch.ts +16 -3
  31. package/src/agent/notifications.ts +5 -5
  32. package/src/agent/parking.ts +39 -17
  33. package/src/agent/runner.ts +20 -21
  34. package/src/agent/tool-schemas.ts +2 -0
  35. package/src/agent/usage.ts +48 -3
  36. package/src/agent/view.ts +15 -13
  37. package/src/agent/wake.ts +19 -15
  38. package/src/pi-intercom/broker/broker.ts +2 -1
  39. package/src/pi-intercom/broker/snapshots.ts +82 -37
  40. package/src/pi-intercom/broker/spawn-claim.ts +98 -0
  41. package/src/pi-intercom/broker/spawn.ts +42 -82
  42. package/src/pi-intercom/lifecycle.ts +38 -27
  43. package/src/shared/post-exit-stdio-guard.ts +48 -40
@@ -8,12 +8,8 @@ export function isContextRequest(value) {
8
8
  }
9
9
  /** Owns native context validity and occupancy, never journal or socket state. */
10
10
  export class IntercomLifecycle {
11
- context = null;
12
- sessionId = null;
11
+ state = { kind: "inactive", epoch: 0 };
13
12
  model = "unknown";
14
- disposed = true;
15
- epoch = 0;
16
- incarnation = null;
17
13
  peerId = null;
18
14
  guardedDelivery = false;
19
15
  applicationGuard;
@@ -29,10 +25,10 @@ export class IntercomLifecycle {
29
25
  this.lineage = lineage;
30
26
  }
31
27
  get generation() {
32
- return this.epoch;
28
+ return this.state.epoch;
33
29
  }
34
30
  get currentSessionId() {
35
- return this.sessionId;
31
+ return this.state.kind === "inactive" ? null : this.state.sessionId;
36
32
  }
37
33
  get toolCount() {
38
34
  return this.tools.size;
@@ -64,7 +60,7 @@ export class IntercomLifecycle {
64
60
  request.respond(this.snapshot());
65
61
  }
66
62
  snapshot() {
67
- if (!this.live()) {
63
+ if (this.state.kind !== "live" || !this.live()) {
68
64
  return {
69
65
  version: 1,
70
66
  rootSessionId: null,
@@ -76,10 +72,10 @@ export class IntercomLifecycle {
76
72
  }
77
73
  return {
78
74
  version: 1,
79
- rootSessionId: this.lineage?.rootSessionId ?? this.sessionId,
75
+ rootSessionId: this.lineage?.rootSessionId ?? this.state.sessionId,
80
76
  parentSessionId: this.lineage?.parentSessionId,
81
77
  peerId: this.peerId,
82
- incarnation: this.incarnation,
78
+ incarnation: this.state.incarnation,
83
79
  connected: this.peerId !== null,
84
80
  guardedDelivery: this.guardedDelivery ? 1 : null,
85
81
  applicationGuard: this.applicationGuard,
@@ -107,36 +103,40 @@ export class IntercomLifecycle {
107
103
  this.applicationGuard === guard.applicationGuard);
108
104
  }
109
105
  start(ctx) {
110
- this.disposed = false;
111
- this.epoch += 1;
112
- this.context = ctx;
113
- this.sessionId = ctx.sessionManager.getSessionId();
114
- this.incarnation = randomUUID();
106
+ this.state = {
107
+ kind: "live",
108
+ epoch: this.state.epoch + 1,
109
+ context: ctx,
110
+ sessionId: ctx.sessionManager.getSessionId(),
111
+ incarnation: randomUUID(),
112
+ };
115
113
  this.applicationGuard = undefined;
116
114
  this.model = ctx.model?.id ?? "unknown";
117
115
  this.running = false;
118
116
  this.activity = 0;
119
117
  this.tools.clear();
120
118
  this.transport(null);
121
- return this.epoch;
119
+ return this.state.epoch;
122
120
  }
123
121
  invalidate() {
124
- this.disposed = true;
125
- this.epoch += 1;
122
+ const epoch = this.state.epoch + 1;
123
+ this.state =
124
+ this.state.kind === "inactive"
125
+ ? { kind: "inactive", epoch }
126
+ : { kind: "invalidated", epoch, sessionId: this.state.sessionId };
126
127
  this.running = false;
127
128
  this.tools.clear();
128
129
  this.publishContext();
129
130
  }
130
131
  clear() {
131
- this.context = null;
132
- this.sessionId = null;
132
+ this.state = { kind: "inactive", epoch: this.state.epoch };
133
133
  }
134
- live(ctx = this.context, generation = this.epoch) {
135
- if (this.disposed || generation !== this.epoch || !ctx) {
134
+ live(ctx = this.state.kind === "live" ? this.state.context : null, generation = this.state.epoch) {
135
+ if (this.state.kind !== "live" || generation !== this.state.epoch || !ctx) {
136
136
  return null;
137
137
  }
138
138
  try {
139
- if (this.sessionId !== null && ctx.sessionManager.getSessionId() !== this.sessionId) {
139
+ if (ctx.sessionManager.getSessionId() !== this.state.sessionId) {
140
140
  return null;
141
141
  }
142
142
  // Reading a native property detects invalidated contexts without modifying them.
@@ -209,12 +209,12 @@ export class IntercomLifecycle {
209
209
  }
210
210
  async registration(pendingAsks) {
211
211
  const live = this.live();
212
- if (!live || this.sessionId === null) {
212
+ if (!live || this.state.kind !== "live") {
213
213
  throw new Error("Intercom runtime not initialized");
214
214
  }
215
215
  return {
216
- ...this.identity(this.sessionId),
217
- incarnation: this.incarnation ?? undefined,
216
+ ...this.identity(this.state.sessionId),
217
+ incarnation: this.state.incarnation,
218
218
  guardedDelivery: 1,
219
219
  applicationGuard: this.applicationGuard,
220
220
  cwd: live.cwd,
@@ -280,12 +280,13 @@ export function attachChildProcessLifecycle(child) {
280
280
  return new OwnedChildLifecycle(child);
281
281
  }
282
282
  function attachPostExitStdioGuard(child) {
283
- let exited = false;
283
+ let state = { kind: "waiting" };
284
284
  let stdoutEnded = false;
285
285
  let stderrEnded = false;
286
- let idleTimer;
287
- let hardTimer;
288
286
  const destroyUnendedStdio = () => {
287
+ if (state.kind !== "draining") {
288
+ return;
289
+ }
289
290
  if (!stdoutEnded) {
290
291
  try {
291
292
  child.stdout?.destroy();
@@ -302,51 +303,57 @@ function attachPostExitStdioGuard(child) {
302
303
  // The process has exited; failed pipe destruction must not prevent other cleanup.
303
304
  }
304
305
  }
305
- };
306
- const clearTimers = () => {
307
- if (idleTimer) {
308
- clearTimeout(idleTimer);
309
- idleTimer = undefined;
310
- }
311
- if (hardTimer) {
312
- clearTimeout(hardTimer);
313
- hardTimer = undefined;
314
- }
306
+ cleanup();
315
307
  };
316
308
  const armIdleTimer = () => {
317
- if (!exited) {
309
+ if (state.kind !== "draining") {
318
310
  return;
319
311
  }
320
- if (idleTimer) {
321
- clearTimeout(idleTimer);
322
- }
323
- idleTimer = setTimeout(destroyUnendedStdio, 2000);
324
- idleTimer.unref();
312
+ clearTimeout(state.idle);
313
+ state.idle = setTimeout(destroyUnendedStdio, 2000);
314
+ state.idle.unref();
325
315
  };
326
- child.stdout?.on("data", armIdleTimer);
327
- child.stderr?.on("data", armIdleTimer);
328
- child.stdout?.on("end", () => {
316
+ const stdoutEnd = () => {
329
317
  stdoutEnded = true;
330
318
  if (stderrEnded) {
331
- clearTimers();
319
+ cleanup();
332
320
  }
333
- });
334
- child.stderr?.on("end", () => {
321
+ };
322
+ const stderrEnd = () => {
335
323
  stderrEnded = true;
336
324
  if (stdoutEnded) {
337
- clearTimers();
325
+ cleanup();
338
326
  }
339
- });
340
- child.on("exit", () => {
341
- exited = true;
342
- armIdleTimer();
343
- if (hardTimer) {
327
+ };
328
+ const exited = () => {
329
+ if (state.kind !== "waiting") {
344
330
  return;
345
331
  }
346
- hardTimer = setTimeout(destroyUnendedStdio, 8000);
347
- hardTimer.unref();
348
- });
349
- child.on("close", clearTimers);
350
- child.on("error", clearTimers);
351
- return clearTimers;
332
+ const hard = setTimeout(destroyUnendedStdio, 8000);
333
+ hard.unref();
334
+ state = { kind: "draining", idle: undefined, hard };
335
+ armIdleTimer();
336
+ };
337
+ function cleanup() {
338
+ if (state.kind === "draining") {
339
+ clearTimeout(state.idle);
340
+ clearTimeout(state.hard);
341
+ }
342
+ state = { kind: "closed" };
343
+ child.stdout?.off("data", armIdleTimer);
344
+ child.stderr?.off("data", armIdleTimer);
345
+ child.stdout?.off("end", stdoutEnd);
346
+ child.stderr?.off("end", stderrEnd);
347
+ child.off("exit", exited);
348
+ child.off("close", cleanup);
349
+ child.off("error", cleanup);
350
+ }
351
+ child.stdout?.on("data", armIdleTimer);
352
+ child.stderr?.on("data", armIdleTimer);
353
+ child.stdout?.on("end", stdoutEnd);
354
+ child.stderr?.on("end", stderrEnd);
355
+ child.on("exit", exited);
356
+ child.on("close", cleanup);
357
+ child.on("error", cleanup);
358
+ return cleanup;
352
359
  }
@@ -23,6 +23,12 @@ Run narrow owner-boundary tests locally through `scripts/run-tests.mjs`. Full
23
23
  `npm run ci` belongs in hosted CI and includes package/install smokes and the
24
24
  complete suites. Keep the strict Oxlint/Oxfmt rules; see [code quality](code-quality.md).
25
25
 
26
+ Builds compile into process-owned staging before replacing `dist`. If a publisher
27
+ exits after retiring the previous build, later builds preserve its complete
28
+ `.previous` tree until a new complete build publishes. A failed build reports
29
+ the retained recovery paths. Recovery never promotes an older backup into a
30
+ concurrent publisher's retirement gap or reaps live or uncertain owners.
31
+
26
32
  ## Native Android/Termux
27
33
 
28
34
  Use app-private storage, not Android's shared `/sdcard` filesystem. No Termux app
@@ -80,6 +86,25 @@ npm and attached to the matching GitHub release.
80
86
 
81
87
  ## Isolation and safety
82
88
 
89
+ Parent attachment keeps one binding through detached, waiting, attaching and
90
+ attached phases. The existing shutdown signal closes the owner. Managed
91
+ readiness can attach only its own still-current waiting binding; shutdown joins
92
+ admitted publication before releasing the matching generation.
93
+
94
+ Parking returns resident, exit-with-parked-authority or exit-after-failed-save.
95
+ Both exit outcomes have disposed the native runtime and must exit the actor;
96
+ neither permits a second ordinary disposal. Restore tracks unopened, opening
97
+ and adopted phases, keeping the exact saved cut with the phase that owns it.
98
+
99
+ Directory watches distinguish unarmed, armed-to-one-inode and polling-only
100
+ availability. Missing paths can be armed later; unsupported or failed native
101
+ watches keep the existing polling fallback. Each coalesced Wake has one waiting
102
+ owner loop.
103
+
104
+ Smoke child cleanup treats leader exit and pipe closure separately. Pipe draining
105
+ has waiting, draining and closed phases. Closing it cancels its deadlines and
106
+ detaches its listeners, so later events cannot restart cleanup.
107
+
83
108
  Every test/smoke broker uses a short, explicit `PI_INTERCOM_RUNTIME_DIR` owned by
84
109
  the fixture. Do not touch `/tmp/pi-intercom-<uid>` to test namespace behavior on a
85
110
  workstation with active sessions. Tests for the default path inspect it without
package/docs/intercom.md CHANGED
@@ -341,6 +341,9 @@ tool to give up the turn.
341
341
 
342
342
  Shutdown joins admitted inbound work before detaching its native owner. Stale
343
343
  runtime generations must not checkpoint or deliver into a replacement session.
344
+ Context state is inactive, live with its session and incarnation, or invalidated
345
+ while transport cleanup retains the session ID. Clearing cleanup drops that ID;
346
+ transport connection and native tool activity remain separate from context validity.
344
347
  The native tests own these contracts; neither a timer nor a source-string test is
345
348
  publication evidence.
346
349
 
@@ -396,4 +399,14 @@ records include native boot/namespace/start identity to reject proven PID reuse;
396
399
  ambiguous legacy numeric PIDs remain blocking. A process is never killed merely
397
400
  to recover broker startup.
398
401
 
402
+ Startup distinguishes absent, live and uncertain ownership. An unreadable or
403
+ invalid PID record is uncertain, not permission to launch another broker.
404
+ A startup claim stays owned while its process is live, regardless of age.
405
+ Only a proven exited or replaced owner permits reclaiming it. Linux claims carry
406
+ the same native process identity as broker PID records. Legacy or incomparable
407
+ identities remain blocking; a timestamp alone cannot prove PID reuse.
408
+ Each starter keeps the claim's
409
+ file descriptor open and releases only the same device/inode, leaving a
410
+ replacement claim untouched.
411
+
399
412
  Remote authenticated mail is separate: see [bridge setup](intercom-bridge.md).
package/docs/reference.md CHANGED
@@ -12,6 +12,7 @@ delegate({
12
12
  name: "optional-readable-name",
13
13
  cwd: "/optional/working/directory",
14
14
  model: "provider/model",
15
+ thinking: "low",
15
16
  context: "fresh",
16
17
  });
17
18
  ```
@@ -21,19 +22,27 @@ canned profile. `name` is an optional display name, not a unique identity or rol
21
22
  Omitted cwd/model inherit the invoking session's effective selection. A supplied
22
23
  model must be available; there is no hidden alternate-model retry catalog.
23
24
 
25
+ `thinking` accepts `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`.
26
+ It overrides parent effort for this child without changing the parent. It works
27
+ with an omitted model or a literal model ID on every supported host, including
28
+ official Pi 1.5.0. Omission preserves inherited effort. Pi applies its native
29
+ model-capability clamping; the launch receipt records the requested value, not
30
+ proof of the child's effective effort.
31
+
24
32
  On hosts exporting Pi's native `resolveModelReference`, `model` also accepts
25
33
  `provider/model:thinking` (for example `provider/model:low` or `provider/model:off`).
26
- The explicit suffix overrides inherited thinking for that child only. Literal
34
+ The explicit suffix overrides inherited thinking for that child only. If both
35
+ `thinking` and a suffix specify effort, they must match or the launch fails. Literal
27
36
  model IDs take precedence, including IDs containing colons or additional slashes;
28
37
  unknown IDs and invalid suffixes fail without fuzzy matching or another provider.
29
38
  Native resolution follows Pi's case-insensitive ID matching. Pi still applies the
30
39
  selected model's native thinking capabilities.
31
40
 
32
41
  Official Pi, including 1.5.0, does not export that resolver. Older fork runtimes also
33
- lack the export. Those hosts retain
34
- case-sensitive literal-ID overrides and inherited thinking; suffix requests fail with an upgrade
35
- message rather than being parsed by an extension-side replacement. This requires
36
- a host with the public resolver export, not merely a matching version number.
42
+ lack the export. Those hosts retain case-sensitive literal-ID overrides and
43
+ support the `thinking` field. Suffix requests fail with an upgrade message rather
44
+ than being parsed by an extension-side replacement. Suffix support requires a
45
+ host with the public resolver export, not merely a matching version number.
37
46
 
38
47
  Fresh context is the default. Fork uses the native conversation, including
39
48
  historical tool calls and results; restricting a child's authority does not erase
@@ -58,6 +67,8 @@ tool or programmatic invocation of `delegate` or `agent_runs`.
58
67
 
59
68
  Saved runs belong to the native main session that launched them, not its cwd,
60
69
  project, display name, or a different session forked from its history.
70
+ Live guidance and saved-run successors keep the child's saved effort even if the
71
+ parent's effort has changed.
61
72
 
62
73
  | Action | Purpose |
63
74
  | ---------- | ------------------------------------------------------------------------------------ |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitchmultz/pi-subagents",
3
- "version": "0.52.18",
3
+ "version": "0.52.19",
4
4
  "description": "Pi extension for subagent orchestration and local session coordination",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/scripts/build.mjs CHANGED
@@ -67,21 +67,37 @@ async function hasPublishedDist(/** @type {string} */ path) {
67
67
  }
68
68
  }
69
69
 
70
- async function reapStrandedStaging(/** @type {string} */ cwd) {
70
+ /**
71
+ * @param {string} cwd
72
+ * @param {import("node:fs").Dirent} entry
73
+ * @returns {{ path: string, kind: "staging" | "previous" } | undefined}
74
+ */
75
+ function strandedBuild(cwd, entry) {
71
76
  // Signal 0 proves only that the owner was gone at probe time. Pid reuse
72
77
  // between this probe and removal is inherent to pid-based reaping.
73
- for (const entry of await readdir(cwd, { withFileTypes: true })) {
74
- const match = /^dist\.staging\.(\d+)(?:\.previous)?$/.exec(entry.name);
75
- if (!match || !entry.isDirectory()) {
76
- continue;
77
- }
78
- const ownerPid = Number(match[1]);
79
- if (ownerPid === process.pid || !isOwnerGone(ownerPid)) {
80
- continue;
78
+ const match = /^dist\.staging\.(\d+)(\.previous)?$/.exec(entry.name);
79
+ if (!match || !entry.isDirectory()) {
80
+ return;
81
+ }
82
+ const ownerPid = Number(match[1]);
83
+ if (ownerPid === process.pid || !isOwnerGone(ownerPid)) {
84
+ return;
85
+ }
86
+ return { path: join(cwd, entry.name), kind: match[2] === undefined ? "staging" : "previous" };
87
+ }
88
+
89
+ async function reapStrandedBuilds(/** @type {string} */ cwd) {
90
+ const stranded = (await readdir(cwd, { withFileTypes: true })).flatMap((entry) => {
91
+ const build = strandedBuild(cwd, entry);
92
+ return build ? [build] : [];
93
+ });
94
+ for (const entry of stranded) {
95
+ if (entry.kind === "staging") {
96
+ await discardStaging(entry.path);
81
97
  }
82
- // Reap abandoned trees sequentially to bound filesystem removal pressure.
83
- await discardStaging(join(cwd, entry.name));
84
98
  }
99
+ // Never promote rollback material into another live publisher's retirement gap.
100
+ return stranded.filter((entry) => entry.kind === "previous").map((entry) => entry.path);
85
101
  }
86
102
 
87
103
  async function compileProject(
@@ -208,13 +224,24 @@ async function publishStaging(/** @type {string} */ stagingDir, /** @type {strin
208
224
  async function main() {
209
225
  const cwd = process.cwd();
210
226
  const compiler = compilerInvocation(process.argv[2]);
211
- await reapStrandedStaging(cwd);
227
+ const previous = await reapStrandedBuilds(cwd);
212
228
  // Pid-scoped staging isolates concurrent emits; only a complete tree publishes.
213
229
  const stagingDir = join(cwd, `dist.staging.${process.pid}`);
214
230
  await rm(stagingDir, RM_OPTIONS);
215
231
  await rm(`${stagingDir}.previous`, RM_OPTIONS);
216
- await compileToStaging(cwd, stagingDir, compiler);
217
- await publishStaging(stagingDir, join(cwd, "dist"));
232
+ try {
233
+ await compileToStaging(cwd, stagingDir, compiler);
234
+ await publishStaging(stagingDir, join(cwd, "dist"));
235
+ } catch (error) {
236
+ for (const retained of previous.filter(existsSync)) {
237
+ console.warn(`Previous build retained for recovery: ${retained}`);
238
+ }
239
+ throw error;
240
+ }
241
+ // A complete published winner now replaces these older rollback candidates.
242
+ for (const retired of previous) {
243
+ await discardStaging(retired);
244
+ }
218
245
  }
219
246
 
220
247
  main().catch((/** @type {unknown} */ error) => {
@@ -14,6 +14,7 @@ profile discovery, loader, role body, chain DSL, or nested delegation.
14
14
  delegate({
15
15
  prompt: "Complete assignment with constraints and expected output",
16
16
  name: "short-name",
17
+ thinking: "low",
17
18
  });
18
19
  ```
19
20
 
@@ -23,12 +24,14 @@ expected result, and stop boundary. Do not rely on a hidden profile to add them.
23
24
 
24
25
  Fresh context is default; `context:"fork"` preserves the native conversation,
25
26
  including prior tool results. A forked child still cannot delegate. Optional
26
- `cwd` and `model` are explicit launch overrides; do not change global auth/model
27
- settings to implement an override.
27
+ `cwd`, `model`, and `thinking` are explicit launch overrides; do not change global
28
+ auth/model settings to implement an override. `thinking` accepts `off`, `minimal`,
29
+ `low`, `medium`, `high`, `xhigh`, or `max`. Omit it to inherit the parent's effort.
30
+ Pi clamps the child's effort to its model capabilities without changing the parent.
28
31
  `model:"provider/model:thinking"` uses Pi's native suffix resolver when the host
29
32
  exports it; a literal model ID wins before suffix parsing. Without that host API,
30
- use a literal ID with inherited thinking or upgrade Pi. Never retry another model
31
- silently.
33
+ use `thinking` with an omitted model or literal ID. If both the field and a native
34
+ suffix supply effort, they must match. Never retry another model silently.
32
35
 
33
36
  Every launch, continuation and guidance call returns a receipt; no tool waits for
34
37
  child completion. Continue useful independent work; when child evidence gates the
@@ -45,6 +48,7 @@ invocation exits after its final turn and stops any remaining children.
45
48
  - `inspect` reads a run by ID; follow returned offsets for complete output.
46
49
  - `continue` takes a complete follow-up `prompt` and keeps the saved child chat.
47
50
  Active work receives guidance; completed work gets an immutable successor.
51
+ Both keep the child's saved effort, independent of later parent changes.
48
52
  - `stop` requests termination. Inspect actual process evidence; request acceptance
49
53
  does not prove actor or command exit.
50
54
 
@@ -93,6 +93,23 @@ export const resultSchema = Type.Object({
93
93
  ),
94
94
  });
95
95
  export type Result = Static<typeof resultSchema>;
96
+ export type AccountingState =
97
+ | { readonly kind: "unrecorded"; readonly terminalEntryId: string | undefined }
98
+ | {
99
+ readonly kind: "verified";
100
+ readonly contributions: Contribution[];
101
+ readonly terminalEntryId: string | undefined;
102
+ }
103
+ | {
104
+ readonly kind: "pending";
105
+ readonly pending: NonNullable<Result["accountingPending"]>;
106
+ readonly error: string | undefined;
107
+ }
108
+ | {
109
+ readonly kind: "unavailable";
110
+ readonly error: string;
111
+ readonly terminalEntryId: string | undefined;
112
+ };
96
113
  export const commandSchema = Type.Object({
97
114
  id: Type.String(),
98
115
  runId: Type.String(),
package/src/agent/host.ts CHANGED
@@ -9,6 +9,25 @@ import { readIntercomLineage } from "../pi-intercom/identity.ts";
9
9
 
10
10
  export { errorMessage as errorText } from "../shared/unknown.ts";
11
11
 
12
+ /** Native fork lifetime owner; structural detection keeps official Pi supported. */
13
+ export interface ManagedHost {
14
+ readonly owner: { readonly pid: number };
15
+ readonly ready: Promise<boolean>;
16
+ }
17
+ export function managedHost(context: SDK.ExtensionContext): ManagedHost | undefined {
18
+ const host: unknown = Reflect.get(context, "managedHost");
19
+ if (typeof host !== "object" || host === null) {
20
+ return undefined;
21
+ }
22
+ const owner: unknown = Reflect.get(host, "owner");
23
+ const pid: unknown =
24
+ typeof owner === "object" && owner !== null ? Reflect.get(owner, "pid") : undefined;
25
+ const ready: unknown = Reflect.get(host, "ready");
26
+ return typeof pid === "number" && Number.isSafeInteger(pid) && ready instanceof Promise
27
+ ? { owner: { pid }, ready: ready.then((accepted) => accepted === true) }
28
+ : undefined;
29
+ }
30
+
12
31
  /** A launcher may still own children while handing off or replacing a worker. */
13
32
  export function retainsChildren(event: object): boolean {
14
33
  const disposition: unknown = Reflect.get(event, "disposition");