@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.
- package/CHANGELOG.md +34 -0
- package/README.md +4 -0
- package/dist/agent/host.js +12 -0
- package/dist/agent/index.js +70 -56
- package/dist/agent/launch.js +12 -3
- package/dist/agent/notifications.js +5 -5
- package/dist/agent/parking.js +26 -15
- package/dist/agent/runner.js +18 -16
- package/dist/agent/tool-schemas.js +2 -0
- package/dist/agent/usage.js +42 -3
- package/dist/agent/view.js +13 -13
- package/dist/agent/wake.js +17 -15
- package/dist/build-info.js +1 -1
- package/dist/pi-intercom/broker/broker.js +2 -2
- package/dist/pi-intercom/broker/snapshots.d.ts +25 -7
- package/dist/pi-intercom/broker/snapshots.js +56 -25
- package/dist/pi-intercom/broker/spawn-claim.js +89 -0
- package/dist/pi-intercom/broker/spawn.js +31 -78
- package/dist/pi-intercom/lifecycle.js +26 -26
- package/dist/shared/post-exit-stdio-guard.js +44 -37
- package/docs/development.md +25 -0
- package/docs/intercom.md +13 -0
- package/docs/reference.md +16 -5
- package/package.json +1 -1
- package/scripts/build.mjs +41 -14
- package/skills/pi-subagents/SKILL.md +8 -4
- package/src/agent/contracts.ts +17 -0
- package/src/agent/host.ts +19 -0
- package/src/agent/index.ts +91 -62
- package/src/agent/launch.ts +16 -3
- package/src/agent/notifications.ts +5 -5
- package/src/agent/parking.ts +39 -17
- package/src/agent/runner.ts +20 -21
- package/src/agent/tool-schemas.ts +2 -0
- package/src/agent/usage.ts +48 -3
- package/src/agent/view.ts +15 -13
- package/src/agent/wake.ts +19 -15
- package/src/pi-intercom/broker/broker.ts +2 -1
- package/src/pi-intercom/broker/snapshots.ts +82 -37
- package/src/pi-intercom/broker/spawn-claim.ts +98 -0
- package/src/pi-intercom/broker/spawn.ts +42 -82
- package/src/pi-intercom/lifecycle.ts +38 -27
- 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
|
|
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.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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.
|
|
153
|
-
this.
|
|
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.
|
|
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.
|
|
177
|
+
if (this.state.kind !== "live" || generation !== this.state.epoch || !ctx) {
|
|
167
178
|
return null;
|
|
168
179
|
}
|
|
169
180
|
try {
|
|
170
|
-
if (
|
|
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.
|
|
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
|
|
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
|
|
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 (
|
|
389
|
+
if (state.kind !== "draining") {
|
|
395
390
|
return;
|
|
396
391
|
}
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
399
|
+
cleanup();
|
|
410
400
|
}
|
|
411
|
-
}
|
|
412
|
-
|
|
401
|
+
};
|
|
402
|
+
const stderrEnd = () => {
|
|
413
403
|
stderrEnded = true;
|
|
414
404
|
if (stdoutEnded) {
|
|
415
|
-
|
|
405
|
+
cleanup();
|
|
416
406
|
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
armIdleTimer();
|
|
421
|
-
if (hardTimer) {
|
|
407
|
+
};
|
|
408
|
+
const exited = () => {
|
|
409
|
+
if (state.kind !== "waiting") {
|
|
422
410
|
return;
|
|
423
411
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
}
|