@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
@@ -24,14 +24,21 @@ type ContextSnapshot = Readonly<Record<keyof ContextTarget, string | null>> &
24
24
  readonly guardedDelivery: 1 | null;
25
25
  };
26
26
 
27
+ type ContextState =
28
+ | { readonly kind: "inactive"; readonly epoch: number }
29
+ | { readonly kind: "invalidated"; readonly epoch: number; readonly sessionId: string }
30
+ | {
31
+ readonly kind: "live";
32
+ readonly epoch: number;
33
+ readonly context: ExtensionContext;
34
+ readonly sessionId: string;
35
+ readonly incarnation: string;
36
+ };
37
+
27
38
  /** Owns native context validity and occupancy, never journal or socket state. */
28
39
  export class IntercomLifecycle {
29
- private context: ExtensionContext | null = null;
30
- private sessionId: string | null = null;
40
+ private state: ContextState = { kind: "inactive", epoch: 0 };
31
41
  private model = "unknown";
32
- private disposed = true;
33
- private epoch = 0;
34
- private incarnation: string | null = null;
35
42
  private peerId: string | null = null;
36
43
  private guardedDelivery = false;
37
44
  private applicationGuard: string | undefined;
@@ -47,10 +54,10 @@ export class IntercomLifecycle {
47
54
  this.lineage = lineage;
48
55
  }
49
56
  get generation(): number {
50
- return this.epoch;
57
+ return this.state.epoch;
51
58
  }
52
59
  get currentSessionId(): string | null {
53
- return this.sessionId;
60
+ return this.state.kind === "inactive" ? null : this.state.sessionId;
54
61
  }
55
62
  get toolCount(): number {
56
63
  return this.tools.size;
@@ -88,7 +95,7 @@ export class IntercomLifecycle {
88
95
  request.respond(this.snapshot());
89
96
  }
90
97
  snapshot(): ContextSnapshot {
91
- if (!this.live()) {
98
+ if (this.state.kind !== "live" || !this.live()) {
92
99
  return {
93
100
  version: 1,
94
101
  rootSessionId: null,
@@ -100,10 +107,10 @@ export class IntercomLifecycle {
100
107
  }
101
108
  return {
102
109
  version: 1,
103
- rootSessionId: this.lineage?.rootSessionId ?? this.sessionId,
110
+ rootSessionId: this.lineage?.rootSessionId ?? this.state.sessionId,
104
111
  parentSessionId: this.lineage?.parentSessionId,
105
112
  peerId: this.peerId,
106
- incarnation: this.incarnation,
113
+ incarnation: this.state.incarnation,
107
114
  connected: this.peerId !== null,
108
115
  guardedDelivery: this.guardedDelivery ? 1 : null,
109
116
  applicationGuard: this.applicationGuard,
@@ -135,39 +142,43 @@ export class IntercomLifecycle {
135
142
  );
136
143
  }
137
144
  start(ctx: ExtensionContext): number {
138
- this.disposed = false;
139
- this.epoch += 1;
140
- this.context = ctx;
141
- this.sessionId = ctx.sessionManager.getSessionId();
142
- this.incarnation = randomUUID();
145
+ this.state = {
146
+ kind: "live",
147
+ epoch: this.state.epoch + 1,
148
+ context: ctx,
149
+ sessionId: ctx.sessionManager.getSessionId(),
150
+ incarnation: randomUUID(),
151
+ };
143
152
  this.applicationGuard = undefined;
144
153
  this.model = ctx.model?.id ?? "unknown";
145
154
  this.running = false;
146
155
  this.activity = 0;
147
156
  this.tools.clear();
148
157
  this.transport(null);
149
- return this.epoch;
158
+ return this.state.epoch;
150
159
  }
151
160
  invalidate(): void {
152
- this.disposed = true;
153
- this.epoch += 1;
161
+ const epoch = this.state.epoch + 1;
162
+ this.state =
163
+ this.state.kind === "inactive"
164
+ ? { kind: "inactive", epoch }
165
+ : { kind: "invalidated", epoch, sessionId: this.state.sessionId };
154
166
  this.running = false;
155
167
  this.tools.clear();
156
168
  this.publishContext();
157
169
  }
158
170
  clear(): void {
159
- this.context = null;
160
- this.sessionId = null;
171
+ this.state = { kind: "inactive", epoch: this.state.epoch };
161
172
  }
162
173
  live(
163
- ctx: ExtensionContext | null = this.context,
164
- generation = this.epoch,
174
+ ctx: ExtensionContext | null = this.state.kind === "live" ? this.state.context : null,
175
+ generation = this.state.epoch,
165
176
  ): ExtensionContext | null {
166
- if (this.disposed || generation !== this.epoch || !ctx) {
177
+ if (this.state.kind !== "live" || generation !== this.state.epoch || !ctx) {
167
178
  return null;
168
179
  }
169
180
  try {
170
- if (this.sessionId !== null && ctx.sessionManager.getSessionId() !== this.sessionId) {
181
+ if (ctx.sessionManager.getSessionId() !== this.state.sessionId) {
171
182
  return null;
172
183
  }
173
184
  // Reading a native property detects invalidated contexts without modifying them.
@@ -240,12 +251,12 @@ export class IntercomLifecycle {
240
251
  }
241
252
  async registration(pendingAsks: number): Promise<Omit<SessionInfo, "id">> {
242
253
  const live = this.live();
243
- if (!live || this.sessionId === null) {
254
+ if (!live || this.state.kind !== "live") {
244
255
  throw new Error("Intercom runtime not initialized");
245
256
  }
246
257
  return {
247
- ...this.identity(this.sessionId),
248
- incarnation: this.incarnation ?? undefined,
258
+ ...this.identity(this.state.sessionId),
259
+ incarnation: this.state.incarnation,
249
260
  guardedDelivery: 1,
250
261
  applicationGuard: this.applicationGuard,
251
262
  cwd: live.cwd,
@@ -19,6 +19,7 @@ interface ChildWithPipedStdio {
19
19
  readonly stdout: ChildProcess["stdout"];
20
20
  readonly stderr: ChildProcess["stderr"];
21
21
  readonly on: ChildProcess["on"];
22
+ readonly off: ChildProcess["off"];
22
23
  }
23
24
 
24
25
  interface ChildWithKill {
@@ -356,13 +357,17 @@ export function attachChildProcessLifecycle(
356
357
  }
357
358
 
358
359
  function attachPostExitStdioGuard(child: ChildWithPipedStdio): () => void {
359
- let exited = false;
360
+ let state:
361
+ | { readonly kind: "waiting" }
362
+ | { readonly kind: "draining"; idle: NodeJS.Timeout | undefined; readonly hard: NodeJS.Timeout }
363
+ | { readonly kind: "closed" } = { kind: "waiting" };
360
364
  let stdoutEnded = false;
361
365
  let stderrEnded = false;
362
- let idleTimer: NodeJS.Timeout | undefined;
363
- let hardTimer: NodeJS.Timeout | undefined;
364
366
 
365
367
  const destroyUnendedStdio = () => {
368
+ if (state.kind !== "draining") {
369
+ return;
370
+ }
366
371
  if (!stdoutEnded) {
367
372
  try {
368
373
  child.stdout?.destroy();
@@ -377,55 +382,58 @@ function attachPostExitStdioGuard(child: ChildWithPipedStdio): () => void {
377
382
  // The process has exited; failed pipe destruction must not prevent other cleanup.
378
383
  }
379
384
  }
380
- };
381
-
382
- const clearTimers = () => {
383
- if (idleTimer) {
384
- clearTimeout(idleTimer);
385
- idleTimer = undefined;
386
- }
387
- if (hardTimer) {
388
- clearTimeout(hardTimer);
389
- hardTimer = undefined;
390
- }
385
+ cleanup();
391
386
  };
392
387
 
393
388
  const armIdleTimer = () => {
394
- if (!exited) {
389
+ if (state.kind !== "draining") {
395
390
  return;
396
391
  }
397
- if (idleTimer) {
398
- clearTimeout(idleTimer);
399
- }
400
- idleTimer = setTimeout(destroyUnendedStdio, 2000);
401
- idleTimer.unref();
392
+ clearTimeout(state.idle);
393
+ state.idle = setTimeout(destroyUnendedStdio, 2000);
394
+ state.idle.unref();
402
395
  };
403
-
404
- child.stdout?.on("data", armIdleTimer);
405
- child.stderr?.on("data", armIdleTimer);
406
- child.stdout?.on("end", () => {
396
+ const stdoutEnd = () => {
407
397
  stdoutEnded = true;
408
398
  if (stderrEnded) {
409
- clearTimers();
399
+ cleanup();
410
400
  }
411
- });
412
- child.stderr?.on("end", () => {
401
+ };
402
+ const stderrEnd = () => {
413
403
  stderrEnded = true;
414
404
  if (stdoutEnded) {
415
- clearTimers();
405
+ cleanup();
416
406
  }
417
- });
418
- child.on("exit", () => {
419
- exited = true;
420
- armIdleTimer();
421
- if (hardTimer) {
407
+ };
408
+ const exited = () => {
409
+ if (state.kind !== "waiting") {
422
410
  return;
423
411
  }
424
- hardTimer = setTimeout(destroyUnendedStdio, 8000);
425
- hardTimer.unref();
426
- });
427
- child.on("close", clearTimers);
428
- child.on("error", clearTimers);
429
-
430
- return clearTimers;
412
+ const hard = setTimeout(destroyUnendedStdio, 8000);
413
+ hard.unref();
414
+ state = { kind: "draining", idle: undefined, hard };
415
+ armIdleTimer();
416
+ };
417
+ function cleanup(): void {
418
+ if (state.kind === "draining") {
419
+ clearTimeout(state.idle);
420
+ clearTimeout(state.hard);
421
+ }
422
+ state = { kind: "closed" };
423
+ child.stdout?.off("data", armIdleTimer);
424
+ child.stderr?.off("data", armIdleTimer);
425
+ child.stdout?.off("end", stdoutEnd);
426
+ child.stderr?.off("end", stderrEnd);
427
+ child.off("exit", exited);
428
+ child.off("close", cleanup);
429
+ child.off("error", cleanup);
430
+ }
431
+ child.stdout?.on("data", armIdleTimer);
432
+ child.stderr?.on("data", armIdleTimer);
433
+ child.stdout?.on("end", stdoutEnd);
434
+ child.stderr?.on("end", stderrEnd);
435
+ child.on("exit", exited);
436
+ child.on("close", cleanup);
437
+ child.on("error", cleanup);
438
+ return cleanup;
431
439
  }