@fitchmultz/pi-subagents 0.52.4 → 0.52.6
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 +29 -0
- package/README.md +5 -4
- package/dist/agent/completion-watches.js +61 -0
- package/dist/agent/contracts.js +9 -0
- package/dist/agent/host.js +5 -0
- package/dist/agent/index.js +20 -51
- package/dist/agent/legacy.js +41 -11
- package/dist/agent/notifications.js +2 -2
- package/dist/agent/parking.js +121 -13
- package/dist/agent/process.js +107 -63
- package/dist/agent/receipts.js +21 -4
- package/dist/agent/runner.js +19 -5
- package/dist/agent/shutdown.js +43 -0
- package/dist/agent/view.js +5 -1
- package/dist/build-info.js +1 -1
- package/dist/pi-intercom/broker/broker.js +7 -6
- package/dist/pi-intercom/broker/client-wake.d.ts +17 -0
- package/dist/pi-intercom/broker/client-wake.js +54 -0
- package/dist/pi-intercom/broker/client.d.ts +2 -0
- package/dist/pi-intercom/broker/client.js +23 -16
- package/dist/pi-intercom/broker/parked.js +122 -5
- package/dist/pi-intercom/broker/spawn.js +10 -6
- package/dist/pi-intercom/config.js +3 -2
- package/dist/pi-intercom/connection.js +72 -11
- package/dist/pi-intercom/inbound-delivery.js +10 -5
- package/dist/pi-intercom/index.js +41 -6
- package/dist/pi-intercom/outbound.js +24 -7
- package/dist/pi-intercom/reply-wait.js +6 -1
- package/dist/pi-intercom/tool-results.js +2 -0
- package/dist/pi-intercom/types.d.ts +6 -0
- package/dist/shared/background-reporting.js +72 -0
- package/dist/shared/parking.js +6 -0
- package/docs/development.md +15 -3
- package/docs/intercom.md +14 -0
- package/docs/performance.md +2 -2
- package/docs/reference.md +13 -5
- package/package.json +1 -1
- package/scripts/package-smoke.mjs +21 -3
- package/src/agent/completion-watches.ts +73 -0
- package/src/agent/contracts.ts +11 -0
- package/src/agent/host.ts +6 -0
- package/src/agent/index.ts +24 -54
- package/src/agent/legacy.ts +44 -12
- package/src/agent/notifications.ts +2 -3
- package/src/agent/parking.ts +148 -12
- package/src/agent/process.ts +130 -66
- package/src/agent/receipts.ts +25 -4
- package/src/agent/runner.ts +25 -5
- package/src/agent/shutdown.ts +50 -0
- package/src/agent/view.ts +9 -0
- package/src/pi-intercom/broker/broker.ts +8 -7
- package/src/pi-intercom/broker/client-wake.ts +61 -0
- package/src/pi-intercom/broker/client.ts +23 -17
- package/src/pi-intercom/broker/parked.ts +152 -10
- package/src/pi-intercom/broker/spawn.ts +10 -9
- package/src/pi-intercom/config.ts +5 -2
- package/src/pi-intercom/connection.ts +88 -12
- package/src/pi-intercom/inbound-delivery.ts +21 -6
- package/src/pi-intercom/index.ts +64 -21
- package/src/pi-intercom/outbound.ts +30 -6
- package/src/pi-intercom/reply-wait.ts +6 -1
- package/src/pi-intercom/tool-results.ts +1 -0
- package/src/pi-intercom/types.ts +4 -0
- package/src/shared/background-reporting.ts +78 -0
- package/src/shared/parking.ts +16 -0
|
@@ -116,9 +116,7 @@ class IntercomBroker {
|
|
|
116
116
|
if (currentId === null && msg.type !== "register") {
|
|
117
117
|
throw new Error(`Received ${msg.type} before register`);
|
|
118
118
|
}
|
|
119
|
-
|
|
120
|
-
this.touchActivity(currentId, false);
|
|
121
|
-
}
|
|
119
|
+
this.touchActivity(currentId, false);
|
|
122
120
|
if (msg.type === "register") {
|
|
123
121
|
if (currentId !== null) {
|
|
124
122
|
throw new Error("Received duplicate register message");
|
|
@@ -152,7 +150,8 @@ class IntercomBroker {
|
|
|
152
150
|
break;
|
|
153
151
|
case "park":
|
|
154
152
|
case "unpark":
|
|
155
|
-
|
|
153
|
+
case "wake_result":
|
|
154
|
+
this.parking.handle(socket, currentId, msg);
|
|
156
155
|
break;
|
|
157
156
|
default:
|
|
158
157
|
throw new Error(`Unknown client message type: ${msg.type}`);
|
|
@@ -172,7 +171,7 @@ class IntercomBroker {
|
|
|
172
171
|
topics: resumed?.topics ?? new TopicSnapshot(info),
|
|
173
172
|
topicFrames,
|
|
174
173
|
lastMessages: msg.lastMessages === true,
|
|
175
|
-
parking
|
|
174
|
+
...this.parking.features(msg),
|
|
176
175
|
receiptReads: msg.receiptReads === 1,
|
|
177
176
|
});
|
|
178
177
|
setId(id);
|
|
@@ -185,6 +184,7 @@ class IntercomBroker {
|
|
|
185
184
|
sessionId: id,
|
|
186
185
|
lastMessages: true,
|
|
187
186
|
parking: msg.parking === true,
|
|
187
|
+
...this.parking.capabilities(msg),
|
|
188
188
|
receiptReads: 1,
|
|
189
189
|
...(msg.guardedDelivery === 1 ? { guardedDelivery: 1 } : {}),
|
|
190
190
|
...(topicFrames ? { topicsSupported: true, topicFrames: true } : {}),
|
|
@@ -397,13 +397,14 @@ class IntercomBroker {
|
|
|
397
397
|
accepted: reason === null,
|
|
398
398
|
delivered: reason === null && !queued,
|
|
399
399
|
...(queued ? { queued } : {}),
|
|
400
|
+
...(queued ? { reason: this.parking.queueReason(target, message.id) } : {}),
|
|
400
401
|
...(reason !== null ? { reason } : {}),
|
|
401
402
|
};
|
|
402
403
|
}
|
|
403
404
|
|
|
404
405
|
/** Update liveness/intercom-activity timestamps for a connected session. */
|
|
405
|
-
private touchActivity(sessionId: string, comms: boolean): void {
|
|
406
|
-
const session = this.sessions.get(sessionId);
|
|
406
|
+
private touchActivity(sessionId: string | null, comms: boolean): void {
|
|
407
|
+
const session = sessionId === null ? undefined : this.sessions.get(sessionId);
|
|
407
408
|
if (!session) {
|
|
408
409
|
return;
|
|
409
410
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type net from "node:net";
|
|
2
|
+
import type { EventEmitter } from "node:events";
|
|
3
|
+
import type { WakeResult } from "../types.ts";
|
|
4
|
+
import type { UnknownRecord } from "../../shared/unknown.ts";
|
|
5
|
+
import { validateIntercomMessageSize, writeMessage } from "./framing.ts";
|
|
6
|
+
|
|
7
|
+
/** Negotiated wake outcomes; old brokers and clients keep the original wake event. */
|
|
8
|
+
export class WakeMessages {
|
|
9
|
+
supported = false;
|
|
10
|
+
private readonly events: EventEmitter;
|
|
11
|
+
private readonly socket: () => net.Socket;
|
|
12
|
+
constructor(events: EventEmitter, socket: () => net.Socket) {
|
|
13
|
+
this.events = events;
|
|
14
|
+
this.socket = socket;
|
|
15
|
+
}
|
|
16
|
+
registration(): { wakeResults?: 1; wakeNotices?: 1 } {
|
|
17
|
+
return {
|
|
18
|
+
...(this.events.listenerCount("wake_request") > 0 ? { wakeResults: 1 } : {}),
|
|
19
|
+
...(this.events.listenerCount("wake_failed") > 0 ? { wakeNotices: 1 } : {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
receive(msg: UnknownRecord): void {
|
|
23
|
+
if (msg.type === "wake_registered") {
|
|
24
|
+
if (typeof msg.requestId !== "string") {
|
|
25
|
+
throw new Error("Invalid wake registration");
|
|
26
|
+
}
|
|
27
|
+
this.events.emit("wake_registered", msg.requestId);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (typeof msg.sessionId !== "string") {
|
|
31
|
+
throw new Error("Invalid wake message");
|
|
32
|
+
}
|
|
33
|
+
if (msg.type === "wake") {
|
|
34
|
+
if (this.supported && typeof msg.requestId === "string") {
|
|
35
|
+
this.events.emit("wake_request", msg.sessionId, msg.requestId);
|
|
36
|
+
} else {
|
|
37
|
+
this.events.emit("wake", msg.sessionId);
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (typeof msg.messageId !== "string" || typeof msg.reason !== "string") {
|
|
42
|
+
throw new Error("Invalid wake failure");
|
|
43
|
+
}
|
|
44
|
+
this.events.emit("wake_failed", msg.sessionId, msg.messageId, msg.reason);
|
|
45
|
+
}
|
|
46
|
+
respond(requestId: string, result: WakeResult): void {
|
|
47
|
+
if (!this.supported) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const frame = { type: "wake_result", requestId, result };
|
|
51
|
+
writeMessage(
|
|
52
|
+
this.socket(),
|
|
53
|
+
validateIntercomMessageSize(frame) === null
|
|
54
|
+
? frame
|
|
55
|
+
: {
|
|
56
|
+
...frame,
|
|
57
|
+
result: { status: "failed", reason: "Wake failed; inspect the agent run for details." },
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -6,6 +6,7 @@ import { BrokerConnection, connectBrokerSocket } from "./connection.ts";
|
|
|
6
6
|
import { SnapshotReader } from "./snapshot-reader.ts";
|
|
7
7
|
import { LastMessageRequests, lastRequest } from "./client-last.ts";
|
|
8
8
|
import { ParkRequests } from "./client-park.ts";
|
|
9
|
+
import { WakeMessages } from "./client-wake.ts";
|
|
9
10
|
import { isNativeReadResult } from "./last.ts";
|
|
10
11
|
import {
|
|
11
12
|
errorMessage,
|
|
@@ -66,6 +67,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
66
67
|
private _sessionId: string | null = null;
|
|
67
68
|
private _topicsSupported = false;
|
|
68
69
|
private readonly parking = new ParkRequests();
|
|
70
|
+
readonly wakes = new WakeMessages(this, () => this.requireActiveSocket());
|
|
69
71
|
private guardedDeliverySupported = false;
|
|
70
72
|
private readonly lastRequests: LastMessageRequests;
|
|
71
73
|
private readonly pendingSends = new Map<
|
|
@@ -158,6 +160,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
158
160
|
this._topicsSupported = false;
|
|
159
161
|
this.parking.settle(false);
|
|
160
162
|
this.guardedDeliverySupported = false;
|
|
163
|
+
this.wakes.supported = false;
|
|
161
164
|
this.disconnecting = false;
|
|
162
165
|
this.failPending(error);
|
|
163
166
|
if (established && !expected) {
|
|
@@ -177,6 +180,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
177
180
|
parking: true,
|
|
178
181
|
receiptReads: 1,
|
|
179
182
|
guardedDelivery: 1,
|
|
183
|
+
...this.wakes.registration(),
|
|
180
184
|
});
|
|
181
185
|
} catch (error) {
|
|
182
186
|
if (this.connection === connection) {
|
|
@@ -213,46 +217,47 @@ export class IntercomClient extends EventEmitter {
|
|
|
213
217
|
throw new Error("Invalid broker message");
|
|
214
218
|
}
|
|
215
219
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (this._sessionId === null && type !== "registered") {
|
|
220
|
-
throw new Error(`Received ${type} before registered`);
|
|
220
|
+
if (this._sessionId === null && msg.type !== "registered") {
|
|
221
|
+
throw new Error(`Received ${msg.type} before registered`);
|
|
221
222
|
}
|
|
222
223
|
|
|
223
|
-
switch (type) {
|
|
224
|
+
switch (msg.type) {
|
|
224
225
|
case "registered":
|
|
225
|
-
this.receiveRegistration(
|
|
226
|
+
this.receiveRegistration(msg);
|
|
226
227
|
break;
|
|
227
228
|
case "sessions":
|
|
228
|
-
this.receiveSessions(
|
|
229
|
+
this.receiveSessions(msg);
|
|
229
230
|
break;
|
|
230
231
|
case "last_request":
|
|
231
|
-
this.receiveLastRequest(
|
|
232
|
+
this.receiveLastRequest(msg);
|
|
232
233
|
break;
|
|
233
234
|
case "last_response":
|
|
234
|
-
this.lastRequests.receive(
|
|
235
|
+
this.lastRequests.receive(msg);
|
|
235
236
|
break;
|
|
236
237
|
case "message":
|
|
237
|
-
this.receiveMessage(
|
|
238
|
+
this.receiveMessage(msg);
|
|
238
239
|
break;
|
|
239
240
|
case "delivered":
|
|
240
241
|
case "delivery_queued":
|
|
241
242
|
case "delivery_failed":
|
|
242
|
-
this.receiveDelivery(
|
|
243
|
+
this.receiveDelivery(msg);
|
|
243
244
|
break;
|
|
244
245
|
case "parked":
|
|
245
246
|
this.parking.settle(true);
|
|
246
247
|
break;
|
|
247
|
-
case "wake":
|
|
248
248
|
case "session_left":
|
|
249
|
-
if (typeof
|
|
250
|
-
throw new Error(`Invalid ${type} message`);
|
|
249
|
+
if (typeof msg.sessionId !== "string") {
|
|
250
|
+
throw new Error(`Invalid ${msg.type} message`);
|
|
251
251
|
}
|
|
252
|
-
this.emit(type,
|
|
252
|
+
this.emit(msg.type, msg.sessionId);
|
|
253
|
+
break;
|
|
254
|
+
case "wake":
|
|
255
|
+
case "wake_failed":
|
|
256
|
+
case "wake_registered":
|
|
257
|
+
this.wakes.receive(msg);
|
|
253
258
|
break;
|
|
254
259
|
default:
|
|
255
|
-
throw new Error(`Unknown broker message type: ${type}`);
|
|
260
|
+
throw new Error(`Unknown broker message type: ${msg.type}`);
|
|
256
261
|
}
|
|
257
262
|
}
|
|
258
263
|
|
|
@@ -269,6 +274,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
269
274
|
this.parking.supported = msg.parking === true;
|
|
270
275
|
this.lastRequests.receiptReads = msg.receiptReads === 1;
|
|
271
276
|
this.guardedDeliverySupported = msg.guardedDelivery === 1;
|
|
277
|
+
this.wakes.supported = msg.wakeResults === 1;
|
|
272
278
|
this.connection?.confirmRegistration();
|
|
273
279
|
this.emit("_registered", { type: "registered", sessionId: msg.sessionId });
|
|
274
280
|
}
|
|
@@ -2,19 +2,35 @@ import type net from "net";
|
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
import type { Message, SessionInfo } from "../types.ts";
|
|
4
4
|
import { nativePeerId } from "../session-targets.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { isRecord, type UnknownRecord } from "../../shared/unknown.ts";
|
|
6
|
+
import type { WakeResult } from "../../shared/parking.ts";
|
|
7
|
+
import { validateIntercomMessageSize, writeMessage } from "./framing.ts";
|
|
6
8
|
|
|
7
|
-
type Retained = Array<{
|
|
9
|
+
type Retained = Array<{
|
|
10
|
+
readonly from: SessionInfo;
|
|
11
|
+
readonly sender: net.Socket | undefined;
|
|
12
|
+
readonly message: Message;
|
|
13
|
+
}>;
|
|
14
|
+
interface WakeAttempt {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly owner: net.Socket;
|
|
17
|
+
readonly count: number;
|
|
18
|
+
retry: boolean;
|
|
19
|
+
}
|
|
8
20
|
interface Parked {
|
|
9
21
|
readonly retained: Retained;
|
|
10
22
|
/** The fenced actor's socket closed: input waits for its restored replacement. */
|
|
11
23
|
closed: boolean;
|
|
24
|
+
attempt?: WakeAttempt;
|
|
25
|
+
lastFailure?: string;
|
|
12
26
|
}
|
|
13
27
|
export interface ParkablePeer {
|
|
14
28
|
readonly socket: net.Socket;
|
|
15
29
|
readonly info: SessionInfo;
|
|
16
30
|
/** This client can park itself, or wake and answer for its parked children. */
|
|
17
31
|
readonly parking: boolean;
|
|
32
|
+
readonly wakeResults: boolean;
|
|
33
|
+
readonly wakeNotices: boolean;
|
|
18
34
|
parked?: Parked;
|
|
19
35
|
}
|
|
20
36
|
export function reachable<T extends ParkablePeer>(target?: T): target is T {
|
|
@@ -25,6 +41,17 @@ type Deliver = (id: string, from: SessionInfo, message: Message) => string | nul
|
|
|
25
41
|
function requested(id: unknown): string | undefined {
|
|
26
42
|
return typeof id === "string" && /^[a-zA-Z0-9_-]{8,80}$/.test(id) ? id : undefined;
|
|
27
43
|
}
|
|
44
|
+
function wakeResponse(msg: UnknownRecord): { id: string; result: WakeResult } {
|
|
45
|
+
if (typeof msg.requestId === "string" && isRecord(msg.result)) {
|
|
46
|
+
if (msg.result.status === "requested") {
|
|
47
|
+
return { id: msg.requestId, result: { status: "requested" } };
|
|
48
|
+
}
|
|
49
|
+
if (msg.result.status === "failed" && typeof msg.result.reason === "string") {
|
|
50
|
+
return { id: msg.requestId, result: { status: "failed", reason: msg.result.reason } };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw new Error("Invalid wake response");
|
|
54
|
+
}
|
|
28
55
|
|
|
29
56
|
/** A fenced child keeps its identity and queued input; it never departs while parked. */
|
|
30
57
|
export class ParkedSessions<T extends ParkablePeer> {
|
|
@@ -34,7 +61,21 @@ export class ParkedSessions<T extends ParkablePeer> {
|
|
|
34
61
|
this.sessions = sessions;
|
|
35
62
|
this.deliver = deliver;
|
|
36
63
|
}
|
|
37
|
-
|
|
64
|
+
handle(socket: net.Socket, id: string, msg: UnknownRecord): boolean {
|
|
65
|
+
if (msg.type === "park" || msg.type === "unpark") {
|
|
66
|
+
this.park(socket, id, msg.type === "park");
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
if (msg.type === "wake_result") {
|
|
70
|
+
const owner = this.sessions.get(id);
|
|
71
|
+
if (owner) {
|
|
72
|
+
this.respond(owner, msg);
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
private park(socket: net.Socket, id: string, on: boolean): void {
|
|
38
79
|
const session = this.sessions.get(id);
|
|
39
80
|
if (on && session?.parking === true) {
|
|
40
81
|
session.parked ??= { retained: [], closed: false };
|
|
@@ -75,6 +116,7 @@ export class ParkedSessions<T extends ParkablePeer> {
|
|
|
75
116
|
}
|
|
76
117
|
/** Flushes a restored child's input and re-sends wakes owed to a (re)registered main. */
|
|
77
118
|
registered(id: string, previous?: T): void {
|
|
119
|
+
this.registeredWake(previous);
|
|
78
120
|
this.flush(id, previous?.parked?.retained ?? []);
|
|
79
121
|
for (const child of this.sessions.values()) {
|
|
80
122
|
if (nativePeerId(child.info.rootSessionId ?? "") === id) {
|
|
@@ -82,6 +124,26 @@ export class ParkedSessions<T extends ParkablePeer> {
|
|
|
82
124
|
}
|
|
83
125
|
}
|
|
84
126
|
}
|
|
127
|
+
private registeredWake(previous?: T): void {
|
|
128
|
+
const main = previous && this.main(previous);
|
|
129
|
+
const attempt = previous?.parked?.attempt;
|
|
130
|
+
if (reachable(main) && attempt?.owner === main.socket) {
|
|
131
|
+
writeMessage(main.socket, { type: "wake_registered", requestId: attempt.id });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
capabilities(msg: UnknownRecord): { wakeResults?: 1; wakeNotices?: 1 } {
|
|
135
|
+
return {
|
|
136
|
+
...(msg.wakeResults === 1 ? { wakeResults: 1 } : {}),
|
|
137
|
+
...(msg.wakeNotices === 1 ? { wakeNotices: 1 } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
features(msg: UnknownRecord): Pick<ParkablePeer, "parking" | "wakeResults" | "wakeNotices"> {
|
|
141
|
+
return {
|
|
142
|
+
parking: msg.parking === true,
|
|
143
|
+
wakeResults: msg.wakeResults === 1,
|
|
144
|
+
wakeNotices: msg.wakeNotices === 1,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
85
147
|
/** A transport handoff, not a departure: no session_left and queued input stays. */
|
|
86
148
|
closed(session: T): boolean {
|
|
87
149
|
const { parked } = session;
|
|
@@ -101,12 +163,22 @@ export class ParkedSessions<T extends ParkablePeer> {
|
|
|
101
163
|
);
|
|
102
164
|
}
|
|
103
165
|
retain(target: T | undefined, from: SessionInfo, message: Message): boolean {
|
|
104
|
-
target?.parked?.retained.push({ from, message });
|
|
105
|
-
if (target) {
|
|
166
|
+
target?.parked?.retained.push({ from, message, sender: this.sessions.get(from.id)?.socket });
|
|
167
|
+
if (target && this.waking(message)) {
|
|
106
168
|
this.wake(target);
|
|
107
169
|
}
|
|
108
170
|
return target?.parked !== undefined;
|
|
109
171
|
}
|
|
172
|
+
queueReason(target: T | undefined, messageId: string): string {
|
|
173
|
+
const last = target?.parked?.lastFailure;
|
|
174
|
+
const reason =
|
|
175
|
+
last === undefined
|
|
176
|
+
? "Queued for a parked child; delivery awaits its return."
|
|
177
|
+
: `Queued for a parked child; the last wake failed: ${last} The message may still arrive after a later wake.`;
|
|
178
|
+
return validateIntercomMessageSize({ type: "delivery_queued", messageId, reason }) === null
|
|
179
|
+
? reason
|
|
180
|
+
: "Queued for a parked child; its last wake failed. Inspect the agent run for details.";
|
|
181
|
+
}
|
|
110
182
|
/** Reads of an exited parked child go to its main, which answers from captured history. */
|
|
111
183
|
reader(id: string): T | undefined {
|
|
112
184
|
const child = this.sessions.get(id);
|
|
@@ -115,14 +187,84 @@ export class ParkedSessions<T extends ParkablePeer> {
|
|
|
115
187
|
// Passive breadcrumbs and topic updates wait for a genuine wake; they never spawn the child.
|
|
116
188
|
private wake(child: T): void {
|
|
117
189
|
const main = this.main(child);
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
190
|
+
const parked = child.parked;
|
|
191
|
+
if (
|
|
192
|
+
!reachable(main) ||
|
|
193
|
+
parked?.closed !== true ||
|
|
194
|
+
!parked.retained.some(({ message }) => this.waking(message))
|
|
195
|
+
) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (!main.wakeResults) {
|
|
123
199
|
writeMessage(main.socket, { type: "wake", sessionId: child.info.id });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (parked.attempt?.owner === main.socket) {
|
|
203
|
+
parked.attempt.retry = true;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const attempt = {
|
|
207
|
+
id: randomUUID(),
|
|
208
|
+
owner: main.socket,
|
|
209
|
+
count: parked.retained.length,
|
|
210
|
+
retry: false,
|
|
211
|
+
};
|
|
212
|
+
parked.attempt = attempt;
|
|
213
|
+
writeMessage(main.socket, { type: "wake", sessionId: child.info.id, requestId: attempt.id });
|
|
214
|
+
}
|
|
215
|
+
private waking(message: Message): boolean {
|
|
216
|
+
return (
|
|
217
|
+
message.passive !== true && message.delivery !== "passive" && message.topic === undefined
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
/** Only the main sent this request can report it, and only while that child stays parked. */
|
|
221
|
+
private respond(owner: T, msg: UnknownRecord): void {
|
|
222
|
+
const { id, result } = wakeResponse(msg);
|
|
223
|
+
const child = [...this.sessions.values()].find(
|
|
224
|
+
(peer) =>
|
|
225
|
+
peer.parked?.attempt?.id === id &&
|
|
226
|
+
peer.parked.attempt.owner === owner.socket &&
|
|
227
|
+
this.main(peer) === owner,
|
|
228
|
+
);
|
|
229
|
+
const parked = child?.parked;
|
|
230
|
+
const attempt = parked?.attempt;
|
|
231
|
+
if (!child || !parked || !attempt) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
parked.attempt = undefined;
|
|
235
|
+
if (result.status === "failed") {
|
|
236
|
+
parked.lastFailure = result.reason;
|
|
237
|
+
for (const retained of parked.retained.slice(0, attempt.count)) {
|
|
238
|
+
this.notify(child, retained, result.reason);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (attempt.retry) {
|
|
242
|
+
this.wake(child);
|
|
124
243
|
}
|
|
125
244
|
}
|
|
245
|
+
private notify(child: T, { from, sender, message }: Retained[number], reason: string): void {
|
|
246
|
+
const origin = this.sessions.get(from.id);
|
|
247
|
+
if (
|
|
248
|
+
message.expectsReply !== true ||
|
|
249
|
+
origin?.wakeNotices !== true ||
|
|
250
|
+
origin.socket !== sender ||
|
|
251
|
+
!reachable(origin)
|
|
252
|
+
) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const frame = {
|
|
256
|
+
type: "wake_failed",
|
|
257
|
+
sessionId: child.info.id,
|
|
258
|
+
messageId: message.id,
|
|
259
|
+
reason,
|
|
260
|
+
};
|
|
261
|
+
writeMessage(
|
|
262
|
+
origin.socket,
|
|
263
|
+
validateIntercomMessageSize(frame) === null
|
|
264
|
+
? frame
|
|
265
|
+
: { ...frame, reason: "Wake failed; inspect the agent run for details." },
|
|
266
|
+
);
|
|
267
|
+
}
|
|
126
268
|
private main(child: T): T | undefined {
|
|
127
269
|
const main = this.sessions.get(nativePeerId(child.info.rootSessionId ?? ""));
|
|
128
270
|
return main?.parking === true && main.parked === undefined ? main : undefined;
|
|
@@ -72,9 +72,14 @@ function toError(error: unknown): Error {
|
|
|
72
72
|
export async function spawnBrokerIfNeeded(
|
|
73
73
|
brokerCommand: string,
|
|
74
74
|
brokerArgs: readonly string[],
|
|
75
|
+
warning: (message?: string) => void = (message) => {
|
|
76
|
+
if (message !== undefined) {
|
|
77
|
+
console.warn(message);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
75
80
|
): Promise<void> {
|
|
76
81
|
prepareBrokerSocketPath();
|
|
77
|
-
await
|
|
82
|
+
warning(await previousBrokerWarning());
|
|
78
83
|
|
|
79
84
|
if (await isBrokerRunning()) {
|
|
80
85
|
return;
|
|
@@ -170,24 +175,20 @@ function readLivePid(pidPath: string): number | null {
|
|
|
170
175
|
return isBrokerPidReused(pid, raw) ? null : pid;
|
|
171
176
|
}
|
|
172
177
|
|
|
173
|
-
async function
|
|
178
|
+
async function previousBrokerWarning(): Promise<string | undefined> {
|
|
174
179
|
const previousSockets = getPreviousBrokerSocketPaths();
|
|
175
180
|
for (const socketPath of previousSockets) {
|
|
176
181
|
// Migration probes are sequential and never register or send to old peers.
|
|
177
182
|
// oxlint-disable-next-line no-await-in-loop
|
|
178
183
|
if (isOwnedBrokerSocket(socketPath) && (await checkOneSocket(socketPath))) {
|
|
179
|
-
|
|
180
|
-
"An older agent-directory Intercom broker is still running. Its peers are not part of shared same-user discovery until they reload with the updated extension. Existing sessions and accepted work are untouched; no legacy fallback is used.",
|
|
181
|
-
);
|
|
182
|
-
return;
|
|
184
|
+
return "An older agent-directory Intercom broker is still running. Its peers are not part of shared same-user discovery until they reload with the updated extension. Existing sessions and accepted work are untouched; no legacy fallback is used.";
|
|
183
185
|
}
|
|
184
186
|
}
|
|
185
187
|
const previousPid = readLivePid(join(getPiAgentDir(), "intercom", "broker.pid"));
|
|
186
188
|
if (previousPid !== null) {
|
|
187
|
-
|
|
188
|
-
`Older Intercom broker PID ${previousPid} may still own accepted work. Its peers need to reload with the updated extension to join shared same-user discovery. Nothing was stopped or replaced; no legacy fallback is used.`,
|
|
189
|
-
);
|
|
189
|
+
return `Older Intercom broker PID ${previousPid} may still own accepted work. Its peers need to reload with the updated extension to join shared same-user discovery. Nothing was stopped or replaced; no legacy fallback is used.`;
|
|
190
190
|
}
|
|
191
|
+
return undefined;
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
export async function stopUnhealthyBrokerBeforeSpawn(): Promise<void> {
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getPiAgentDir } from "./agent-dir.ts";
|
|
4
4
|
import { isRecord, isUnknownArray } from "./validation.ts";
|
|
5
|
+
import { errorMessage } from "../shared/unknown.ts";
|
|
5
6
|
|
|
6
7
|
export interface IntercomConfig {
|
|
7
8
|
/** Broker command used to spawn the broker process (for example, Node or Bun). */
|
|
@@ -93,7 +94,7 @@ function configuredTimeout(
|
|
|
93
94
|
}
|
|
94
95
|
return value;
|
|
95
96
|
}
|
|
96
|
-
export function loadConfig(): IntercomConfig {
|
|
97
|
+
export function loadConfig(report: (message: string) => void = console.error): IntercomConfig {
|
|
97
98
|
const configPath = join(getPiAgentDir(), "intercom", "config.json");
|
|
98
99
|
if (!existsSync(configPath)) {
|
|
99
100
|
return { ...defaults };
|
|
@@ -114,7 +115,9 @@ export function loadConfig(): IntercomConfig {
|
|
|
114
115
|
listTimeoutMs: configuredTimeout(parsed, "listTimeoutMs"),
|
|
115
116
|
};
|
|
116
117
|
} catch (error) {
|
|
117
|
-
|
|
118
|
+
report(
|
|
119
|
+
`Failed to load intercom config at ${configPath}: ${errorMessage(error)}. Using defaults.`,
|
|
120
|
+
);
|
|
118
121
|
return { ...defaults };
|
|
119
122
|
}
|
|
120
123
|
}
|