@fitchmultz/pi-subagents 0.50.0 → 0.52.0
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 +63 -2
- package/README.md +36 -5
- package/dist/agent/child-runtime.js +80 -0
- package/dist/agent/contracts.js +15 -24
- package/dist/agent/host.js +3 -16
- package/dist/agent/index.js +212 -192
- package/dist/agent/journal-tail.js +115 -156
- package/dist/agent/launch.js +20 -8
- package/dist/agent/legacy.js +65 -23
- package/dist/agent/notifications.js +138 -47
- package/dist/agent/observation-state.js +246 -0
- package/dist/agent/observation-view.js +258 -0
- package/dist/agent/observation.js +350 -0
- package/dist/agent/observe-card.js +243 -0
- package/dist/agent/observe-chrome.js +85 -0
- package/dist/agent/observe-client.js +285 -0
- package/dist/agent/observe-layout.js +65 -0
- package/dist/agent/observe-reading.js +84 -0
- package/dist/agent/observe-screen.js +400 -96
- package/dist/agent/observe-transcript.js +382 -0
- package/dist/agent/observe.js +250 -43
- package/dist/agent/parking.js +253 -0
- package/dist/agent/process.js +228 -28
- package/dist/agent/receipts.js +92 -20
- package/dist/agent/runner.js +229 -126
- package/dist/agent/shell.js +2 -1
- package/dist/agent/storage.js +47 -12
- package/dist/agent/tool-schemas.js +20 -0
- package/dist/agent/typebox.js +10 -5
- package/dist/agent/usage.js +0 -115
- package/dist/agent/view.js +46 -16
- package/dist/agent/wake.js +99 -0
- package/dist/build-info.js +1 -1
- package/dist/intercom-client.d.ts +26 -0
- package/dist/intercom-client.js +45 -0
- package/dist/pi-intercom/agent-dir.d.ts +1 -0
- package/dist/pi-intercom/bridge-session.js +2 -1
- package/dist/pi-intercom/broker/broker.js +62 -43
- package/dist/pi-intercom/broker/client-last.d.ts +24 -0
- package/dist/pi-intercom/broker/client-last.js +42 -5
- package/dist/pi-intercom/broker/client-park.d.ts +9 -0
- package/dist/pi-intercom/broker/client-park.js +21 -0
- package/dist/pi-intercom/broker/client.d.ts +59 -0
- package/dist/pi-intercom/broker/client.js +55 -29
- package/dist/pi-intercom/broker/connection.d.ts +29 -0
- package/dist/pi-intercom/broker/framing.d.ts +17 -0
- package/dist/pi-intercom/broker/framing.js +10 -7
- package/dist/pi-intercom/broker/guard.js +23 -0
- package/dist/pi-intercom/broker/last.d.ts +30 -0
- package/dist/pi-intercom/broker/last.js +87 -40
- package/dist/pi-intercom/broker/parked.js +107 -0
- package/dist/pi-intercom/broker/paths.d.ts +13 -0
- package/dist/pi-intercom/broker/paths.js +35 -12
- package/dist/pi-intercom/broker/pid.js +5 -1
- package/dist/pi-intercom/broker/server-connection.js +20 -1
- package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
- package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
- package/dist/pi-intercom/broker/targets.d.ts +9 -0
- package/dist/pi-intercom/connection.js +71 -20
- package/dist/pi-intercom/identity.js +6 -5
- package/dist/pi-intercom/inbound-delivery.js +16 -60
- package/dist/pi-intercom/inbound-journal.js +83 -6
- package/dist/pi-intercom/inbound-record.js +18 -1
- package/dist/pi-intercom/inbound.js +22 -37
- package/dist/pi-intercom/index.js +41 -0
- package/dist/pi-intercom/last-message.js +4 -16
- package/dist/pi-intercom/lifecycle.js +88 -3
- package/dist/pi-intercom/outbound.js +5 -15
- package/dist/pi-intercom/reply-tracker.js +7 -6
- package/dist/pi-intercom/reply-wait.js +2 -2
- package/dist/pi-intercom/session-targets.d.ts +24 -0
- package/dist/pi-intercom/tool-registration.js +4 -1
- package/dist/pi-intercom/tool-results.js +5 -7
- package/dist/pi-intercom/tool-schemas.js +12 -26
- package/dist/pi-intercom/types.d.ts +212 -0
- package/dist/pi-intercom/types.js +54 -3
- package/dist/pi-intercom/validation.d.ts +12 -0
- package/dist/pi-intercom/validation.js +3 -0
- package/dist/shared/bounded-json.js +151 -0
- package/dist/shared/compact-cards.js +66 -0
- package/dist/shared/journal-frames.js +3 -6
- package/dist/shared/json-projection.js +267 -129
- package/dist/shared/native-journal.js +123 -56
- package/dist/shared/parking.js +12 -0
- package/dist/shared/post-exit-stdio-guard.js +120 -7
- package/dist/shared/tui-action.js +29 -0
- package/dist/shared/unknown.d.ts +9 -0
- package/docs/code-quality.md +5 -3
- package/docs/development.md +42 -3
- package/docs/intercom-bridge.md +9 -4
- package/docs/intercom.md +228 -7
- package/docs/performance.md +118 -14
- package/docs/reference.md +168 -20
- package/docs/retired-history-maintenance.md +252 -0
- package/package.json +28 -15
- package/scripts/archive-retired-history.mjs +464 -0
- package/scripts/build.mjs +48 -28
- package/scripts/measure-size.mjs +1 -1
- package/scripts/native-package-smoke.mjs +153 -12
- package/scripts/package-smoke.mjs +80 -11
- package/scripts/prepare.mjs +2 -2
- package/scripts/real-pi-live.mjs +377 -0
- package/scripts/real-pi-processes.mjs +6 -1
- package/scripts/real-pi-smoke.mjs +6 -282
- package/scripts/run-tests.mjs +4 -2
- package/scripts/typescript.mjs +35 -0
- package/skills/pi-intercom/SKILL.md +2 -1
- package/skills/pi-subagents/SKILL.md +23 -5
- package/src/agent/child-runtime.ts +105 -0
- package/src/agent/contracts.ts +18 -34
- package/src/agent/host.ts +39 -20
- package/src/agent/index.ts +234 -207
- package/src/agent/journal-tail.ts +141 -153
- package/src/agent/launch.ts +33 -9
- package/src/agent/legacy.ts +85 -31
- package/src/agent/notifications.ts +192 -67
- package/src/agent/observation-state.ts +277 -0
- package/src/agent/observation-view.ts +267 -0
- package/src/agent/observation.ts +409 -0
- package/src/agent/observe-card.ts +304 -0
- package/src/agent/observe-chrome.ts +170 -0
- package/src/agent/observe-client.ts +318 -0
- package/src/agent/observe-layout.ts +79 -0
- package/src/agent/observe-reading.ts +97 -0
- package/src/agent/observe-screen.ts +443 -129
- package/src/agent/observe-transcript.ts +434 -0
- package/src/agent/observe.ts +304 -71
- package/src/agent/parking.ts +341 -0
- package/src/agent/process.ts +280 -37
- package/src/agent/receipts.ts +114 -24
- package/src/agent/runner.ts +227 -149
- package/src/agent/shell.ts +2 -1
- package/src/agent/storage.ts +44 -11
- package/src/agent/tool-schemas.ts +33 -0
- package/src/agent/typebox.ts +12 -7
- package/src/agent/usage.ts +0 -130
- package/src/agent/view.ts +70 -20
- package/src/agent/wake.ts +107 -0
- package/src/intercom-client.ts +108 -0
- package/src/pi-intercom/bridge-session.ts +2 -2
- package/src/pi-intercom/broker/broker.ts +64 -53
- package/src/pi-intercom/broker/client-last.ts +65 -12
- package/src/pi-intercom/broker/client-park.ts +23 -0
- package/src/pi-intercom/broker/client.ts +63 -45
- package/src/pi-intercom/broker/framing.ts +11 -8
- package/src/pi-intercom/broker/guard.ts +33 -0
- package/src/pi-intercom/broker/last.ts +131 -43
- package/src/pi-intercom/broker/parked.ts +130 -0
- package/src/pi-intercom/broker/paths.ts +39 -12
- package/src/pi-intercom/broker/pid.ts +6 -1
- package/src/pi-intercom/broker/server-connection.ts +27 -2
- package/src/pi-intercom/connection.ts +103 -39
- package/src/pi-intercom/identity.ts +8 -5
- package/src/pi-intercom/inbound-delivery.ts +20 -66
- package/src/pi-intercom/inbound-journal.ts +103 -7
- package/src/pi-intercom/inbound-record.ts +21 -4
- package/src/pi-intercom/inbound.ts +32 -49
- package/src/pi-intercom/index.ts +44 -0
- package/src/pi-intercom/last-message.ts +11 -20
- package/src/pi-intercom/lifecycle.ts +117 -14
- package/src/pi-intercom/outbound.ts +7 -16
- package/src/pi-intercom/reply-tracker.ts +8 -6
- package/src/pi-intercom/reply-wait.ts +3 -2
- package/src/pi-intercom/runtime-types.ts +0 -1
- package/src/pi-intercom/tool-registration.ts +4 -1
- package/src/pi-intercom/tool-results.ts +10 -9
- package/src/pi-intercom/tool-schemas.ts +12 -44
- package/src/pi-intercom/types.ts +115 -4
- package/src/pi-intercom/validation.ts +4 -0
- package/src/shared/bounded-json.ts +162 -0
- package/src/shared/compact-cards.ts +118 -0
- package/src/shared/journal-frames.ts +3 -19
- package/src/shared/json-projection.ts +267 -147
- package/src/shared/native-journal.ts +157 -75
- package/src/shared/parking.ts +22 -0
- package/src/shared/post-exit-stdio-guard.ts +165 -7
- package/src/shared/tui-action.ts +39 -0
- package/tsconfig.client.json +8 -0
- package/tsconfig.json +1 -0
- package/src/shared/stream-json.d.ts +0 -10
|
@@ -7,11 +7,12 @@ import { getBrokerRuntimeDir, prepareBrokerSocketPath } from "./paths.ts";
|
|
|
7
7
|
import { brokerPidRecord } from "./pid.ts";
|
|
8
8
|
import { assertBrokerCanStart } from "./spawn.ts";
|
|
9
9
|
import { LastMessageRelay } from "./last.ts";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { canSeeSession, findSessions } from "./targets.ts";
|
|
11
|
+
import { ParkedSessions, reachable, type ParkablePeer } from "./parked.ts";
|
|
12
|
+
import { acceptBrokerConnection, registeredSessionInfo } from "./server-connection.ts";
|
|
13
|
+
import { guardedDeliveryError } from "./guard.ts";
|
|
12
14
|
import {
|
|
13
15
|
compactTopicMessage,
|
|
14
|
-
isSessionRegistration,
|
|
15
16
|
normalizeMessage,
|
|
16
17
|
normalizeSessionInfo,
|
|
17
18
|
type SessionInfo,
|
|
@@ -35,12 +36,12 @@ import {
|
|
|
35
36
|
const PID_PATH = join(getBrokerRuntimeDir(), "broker.pid");
|
|
36
37
|
const REPLACE_DELIVERY_DELAY_MS = 1500;
|
|
37
38
|
|
|
38
|
-
interface ConnectedSession {
|
|
39
|
-
readonly socket: net.Socket;
|
|
39
|
+
interface ConnectedSession extends ParkablePeer {
|
|
40
40
|
info: SessionInfo;
|
|
41
41
|
topics: TopicSnapshot;
|
|
42
42
|
readonly topicFrames: boolean;
|
|
43
43
|
readonly lastMessages: boolean;
|
|
44
|
+
readonly receiptReads: boolean;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
interface PendingReplaceDelivery {
|
|
@@ -53,6 +54,7 @@ interface PendingReplaceDelivery {
|
|
|
53
54
|
class IntercomBroker {
|
|
54
55
|
private readonly sessions = new Map<string, ConnectedSession>();
|
|
55
56
|
private readonly lastMessages = new LastMessageRelay(this.sessions);
|
|
57
|
+
private readonly parking = new ParkedSessions(this.sessions, this.deliverMessage.bind(this));
|
|
56
58
|
private readonly pendingReplaceDeliveries = new Map<string, PendingReplaceDelivery>();
|
|
57
59
|
private readonly server: net.Server;
|
|
58
60
|
private shutdownTimer: NodeJS.Timeout | null = null;
|
|
@@ -95,7 +97,7 @@ class IntercomBroker {
|
|
|
95
97
|
|
|
96
98
|
this.shutdownTimer = setTimeout(() => {
|
|
97
99
|
this.shutdownTimer = null;
|
|
98
|
-
if (this.
|
|
100
|
+
if (this.parking.exited) {
|
|
99
101
|
console.log("No sessions connected, shutting down");
|
|
100
102
|
this.shutdown();
|
|
101
103
|
}
|
|
@@ -129,7 +131,7 @@ class IntercomBroker {
|
|
|
129
131
|
}
|
|
130
132
|
switch (msg.type) {
|
|
131
133
|
case "unregister":
|
|
132
|
-
this.removeSession(currentId);
|
|
134
|
+
this.removeSession(currentId, socket);
|
|
133
135
|
setId(null);
|
|
134
136
|
break;
|
|
135
137
|
case "list":
|
|
@@ -139,7 +141,8 @@ class IntercomBroker {
|
|
|
139
141
|
this.send(socket, msg, currentId);
|
|
140
142
|
break;
|
|
141
143
|
case "last_request":
|
|
142
|
-
|
|
144
|
+
// Reads never spawn a parked child.
|
|
145
|
+
this.lastMessages.request(currentId, msg, (id) => this.parking.reader(id));
|
|
143
146
|
break;
|
|
144
147
|
case "last_response":
|
|
145
148
|
this.lastMessages.respond(currentId, msg);
|
|
@@ -147,31 +150,30 @@ class IntercomBroker {
|
|
|
147
150
|
case "presence":
|
|
148
151
|
this.presence(msg, currentId);
|
|
149
152
|
break;
|
|
153
|
+
case "park":
|
|
154
|
+
case "unpark":
|
|
155
|
+
this.parking.park(socket, currentId, msg.type === "park");
|
|
156
|
+
break;
|
|
150
157
|
default:
|
|
151
158
|
throw new Error(`Unknown client message type: ${msg.type}`);
|
|
152
159
|
}
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
private register(socket: net.Socket, msg: UnknownRecord, setId: (id: string) => void): void {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
assertMainParent(msg.session, this.sessions);
|
|
160
|
-
const requestedId =
|
|
161
|
-
typeof msg.requestedId === "string" && /^[a-zA-Z0-9_-]{8,80}$/.test(msg.requestedId)
|
|
162
|
-
? msg.requestedId
|
|
163
|
-
: undefined;
|
|
164
|
-
const id =
|
|
165
|
-
requestedId !== undefined && !this.sessions.has(requestedId) ? requestedId : randomUUID();
|
|
166
|
-
const info: SessionInfo = { ...msg.session, id, lastSeen: msg.session.lastSeen ?? Date.now() };
|
|
163
|
+
const registered = registeredSessionInfo(msg, this.sessions);
|
|
164
|
+
const { id, resumed } = this.parking.claim(msg.requestedId, registered);
|
|
165
|
+
const info: SessionInfo = { ...registered, id };
|
|
167
166
|
snapshotFrames(randomUUID(), [info], true);
|
|
168
167
|
const topicFrames = msg.topicFrames === true;
|
|
169
168
|
this.sessions.set(id, {
|
|
170
169
|
socket,
|
|
171
170
|
info: messageSender(info),
|
|
172
|
-
topics
|
|
171
|
+
// A woken generation registers no topics; the parked identity's owner keeps them.
|
|
172
|
+
topics: resumed?.topics ?? new TopicSnapshot(info),
|
|
173
173
|
topicFrames,
|
|
174
174
|
lastMessages: msg.lastMessages === true,
|
|
175
|
+
parking: msg.parking === true,
|
|
176
|
+
receiptReads: msg.receiptReads === 1,
|
|
175
177
|
});
|
|
176
178
|
setId(id);
|
|
177
179
|
if (this.shutdownTimer) {
|
|
@@ -182,19 +184,25 @@ class IntercomBroker {
|
|
|
182
184
|
type: "registered",
|
|
183
185
|
sessionId: id,
|
|
184
186
|
lastMessages: true,
|
|
187
|
+
parking: msg.parking === true,
|
|
188
|
+
receiptReads: 1,
|
|
189
|
+
...(msg.guardedDelivery === 1 ? { guardedDelivery: 1 } : {}),
|
|
185
190
|
...(topicFrames ? { topicsSupported: true, topicFrames: true } : {}),
|
|
186
191
|
});
|
|
192
|
+
this.parking.registered(id, resumed);
|
|
187
193
|
}
|
|
188
194
|
|
|
189
|
-
private removeSession(sessionId: string): void {
|
|
195
|
+
private removeSession(sessionId: string, socket: net.Socket): void {
|
|
190
196
|
const session = this.sessions.get(sessionId);
|
|
191
|
-
if (
|
|
197
|
+
if (session?.socket !== socket) {
|
|
192
198
|
return;
|
|
193
199
|
}
|
|
194
|
-
this.sessions.delete(sessionId);
|
|
195
200
|
this.lastMessages.disconnected(sessionId);
|
|
196
|
-
this.
|
|
197
|
-
|
|
201
|
+
if (!this.parking.closed(session)) {
|
|
202
|
+
this.sessions.delete(sessionId);
|
|
203
|
+
this.clearPendingReplaceDeliveries(sessionId);
|
|
204
|
+
this.broadcast({ type: "session_left", sessionId }, session.info);
|
|
205
|
+
}
|
|
198
206
|
this.scheduleShutdownCheck();
|
|
199
207
|
}
|
|
200
208
|
|
|
@@ -213,7 +221,7 @@ class IntercomBroker {
|
|
|
213
221
|
continue;
|
|
214
222
|
}
|
|
215
223
|
const message = publicationMessage(publication, subscription.awaitRelease === true);
|
|
216
|
-
const error = this.validateDeliveryPayload(from, message, target
|
|
224
|
+
const error = this.validateDeliveryPayload(from, message, target);
|
|
217
225
|
if (error !== null) {
|
|
218
226
|
throw new Error(error);
|
|
219
227
|
}
|
|
@@ -296,6 +304,15 @@ class IntercomBroker {
|
|
|
296
304
|
const targets = findSessions(this.sessions, msg.to, currentId);
|
|
297
305
|
const target = targets[0];
|
|
298
306
|
if (targets.length === 1) {
|
|
307
|
+
const guardError = guardedDeliveryError(this.sessions, msg.to, message);
|
|
308
|
+
if (guardError !== null) {
|
|
309
|
+
writeMessage(socket, {
|
|
310
|
+
type: "delivery_failed",
|
|
311
|
+
messageId: message.id,
|
|
312
|
+
reason: guardError,
|
|
313
|
+
});
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
299
316
|
const fromSession = this.sessions.get(currentId);
|
|
300
317
|
if (!fromSession) {
|
|
301
318
|
writeMessage(socket, {
|
|
@@ -343,11 +360,15 @@ class IntercomBroker {
|
|
|
343
360
|
"lastIntercomActivity",
|
|
344
361
|
"subscriptions",
|
|
345
362
|
"topics",
|
|
363
|
+
"applicationGuard",
|
|
346
364
|
]) {
|
|
347
365
|
if (msg[key] !== undefined) {
|
|
348
366
|
update[key] = msg[key];
|
|
349
367
|
}
|
|
350
368
|
}
|
|
369
|
+
if (msg.applicationGuard === null) {
|
|
370
|
+
delete update.applicationGuard;
|
|
371
|
+
}
|
|
351
372
|
const nextInfo = normalizeSessionInfo(update);
|
|
352
373
|
if (!nextInfo) {
|
|
353
374
|
throw new Error("Invalid presence update");
|
|
@@ -366,22 +387,16 @@ class IntercomBroker {
|
|
|
366
387
|
message: Message,
|
|
367
388
|
): SendResult {
|
|
368
389
|
const target = this.sessions.get(toId);
|
|
369
|
-
if (
|
|
370
|
-
return {
|
|
371
|
-
id: message.id,
|
|
372
|
-
accepted: false,
|
|
373
|
-
delivered: false,
|
|
374
|
-
reason: "Recipient disconnected before delivery",
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
if (shouldReplaceDelivery(message, target.info)) {
|
|
390
|
+
if (target && target.parked === undefined && shouldReplaceDelivery(message, target.info)) {
|
|
378
391
|
return this.queueReplaceDelivery(fromId, toId, from, message);
|
|
379
392
|
}
|
|
380
393
|
const reason = this.deliverMessage(toId, from, message);
|
|
394
|
+
const queued = reason === null && target?.parked !== undefined;
|
|
381
395
|
return {
|
|
382
396
|
id: message.id,
|
|
383
397
|
accepted: reason === null,
|
|
384
|
-
delivered: reason === null,
|
|
398
|
+
delivered: reason === null && !queued,
|
|
399
|
+
...(queued ? { queued } : {}),
|
|
385
400
|
...(reason !== null ? { reason } : {}),
|
|
386
401
|
};
|
|
387
402
|
}
|
|
@@ -410,11 +425,7 @@ class IntercomBroker {
|
|
|
410
425
|
from: SessionInfo,
|
|
411
426
|
message: Message,
|
|
412
427
|
): SendResult {
|
|
413
|
-
const validationError = this.validateDeliveryPayload(
|
|
414
|
-
from,
|
|
415
|
-
message,
|
|
416
|
-
this.sessions.get(toId)?.topicFrames === true,
|
|
417
|
-
);
|
|
428
|
+
const validationError = this.validateDeliveryPayload(from, message, this.sessions.get(toId));
|
|
418
429
|
if (validationError !== null) {
|
|
419
430
|
return { id: message.id, accepted: false, delivered: false, reason: validationError };
|
|
420
431
|
}
|
|
@@ -450,33 +461,33 @@ class IntercomBroker {
|
|
|
450
461
|
}
|
|
451
462
|
}
|
|
452
463
|
|
|
464
|
+
/** One admission rule; retained input is checked as the uncompacted envelope any later
|
|
465
|
+
* transport generation accepts. */
|
|
453
466
|
private validateDeliveryPayload(
|
|
454
467
|
from: SessionInfo,
|
|
455
468
|
message: Message,
|
|
456
|
-
|
|
469
|
+
target: ConnectedSession | undefined,
|
|
457
470
|
): string | null {
|
|
471
|
+
const compact = target?.parked === undefined && target?.topicFrames === true;
|
|
458
472
|
return (
|
|
459
473
|
validateIntercomMessageSize({
|
|
460
474
|
type: "message",
|
|
461
475
|
from: messageSender(from),
|
|
462
|
-
message:
|
|
476
|
+
message: compact ? compactTopicMessage(message) : message,
|
|
463
477
|
})?.message ?? null
|
|
464
478
|
);
|
|
465
479
|
}
|
|
466
480
|
|
|
467
481
|
private deliverMessage(toId: string, from: SessionInfo, message: Message): string | null {
|
|
468
482
|
const target = this.sessions.get(toId);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
) {
|
|
475
|
-
return "Recipient disconnected before delivery";
|
|
483
|
+
const refused =
|
|
484
|
+
guardedDeliveryError(this.sessions, toId, message) ??
|
|
485
|
+
this.validateDeliveryPayload(from, message, target);
|
|
486
|
+
if (refused !== null || this.parking.retain(target, from, message)) {
|
|
487
|
+
return refused;
|
|
476
488
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
return validationError;
|
|
489
|
+
if (!reachable(target)) {
|
|
490
|
+
return "Recipient disconnected before delivery";
|
|
480
491
|
}
|
|
481
492
|
this.touchActivity(toId, true);
|
|
482
493
|
try {
|
|
@@ -1,21 +1,63 @@
|
|
|
1
1
|
import type net from "node:net";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { errorMessage, type UnknownRecord } from "../../shared/unknown.ts";
|
|
4
|
-
import
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
isNativeReadQuery,
|
|
6
|
+
isNativeReadResult,
|
|
7
|
+
isLastMessageResult,
|
|
8
|
+
isReceiptResult,
|
|
9
|
+
type NativeReadQuery,
|
|
10
|
+
type NativeReadResult,
|
|
11
|
+
} from "./last.ts";
|
|
12
|
+
import {
|
|
13
|
+
normalizeSessionInfo,
|
|
14
|
+
type LastMessageQuery,
|
|
15
|
+
type LastMessageResult,
|
|
16
|
+
type ReceiptQuery,
|
|
17
|
+
type ReceiptResult,
|
|
18
|
+
type SessionInfo,
|
|
19
|
+
} from "../types.ts";
|
|
6
20
|
import { writeMessage } from "./framing.ts";
|
|
7
21
|
|
|
22
|
+
/** A peer's read of this session or, for a main, of its exited `parked` child. */
|
|
23
|
+
export function lastRequest(msg: UnknownRecord): {
|
|
24
|
+
readonly requestId: string;
|
|
25
|
+
readonly from: SessionInfo;
|
|
26
|
+
readonly query: NativeReadQuery;
|
|
27
|
+
readonly parked?: SessionInfo;
|
|
28
|
+
} {
|
|
29
|
+
const from = normalizeSessionInfo(msg.from);
|
|
30
|
+
if (typeof msg.requestId !== "string" || !from || !isNativeReadQuery(msg.query)) {
|
|
31
|
+
throw new Error("Invalid last-message request");
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
requestId: msg.requestId,
|
|
35
|
+
from,
|
|
36
|
+
query: msg.query,
|
|
37
|
+
parked: normalizeSessionInfo(msg.parked) ?? undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
8
41
|
export class LastMessageRequests {
|
|
42
|
+
/** Negotiated at registration; a failed connection supports neither. */
|
|
43
|
+
lastMessages = false;
|
|
44
|
+
receiptReads = false;
|
|
45
|
+
private readonly timeoutMs: number;
|
|
46
|
+
constructor(timeoutMs: number) {
|
|
47
|
+
this.timeoutMs = timeoutMs;
|
|
48
|
+
}
|
|
9
49
|
private readonly pending = new Map<
|
|
10
50
|
string,
|
|
11
51
|
{
|
|
12
|
-
readonly resolve: (result:
|
|
52
|
+
readonly resolve: (result: NativeReadResult) => void;
|
|
13
53
|
readonly reject: (error: Readonly<Error>) => void;
|
|
14
54
|
readonly timer: NodeJS.Timeout;
|
|
15
55
|
}
|
|
16
56
|
>();
|
|
17
57
|
|
|
18
58
|
fail(error: Readonly<Error>): void {
|
|
59
|
+
this.lastMessages = false;
|
|
60
|
+
this.receiptReads = false;
|
|
19
61
|
for (const pending of this.pending.values()) {
|
|
20
62
|
clearTimeout(pending.timer);
|
|
21
63
|
pending.reject(error);
|
|
@@ -24,7 +66,7 @@ export class LastMessageRequests {
|
|
|
24
66
|
}
|
|
25
67
|
|
|
26
68
|
receive(message: UnknownRecord): void {
|
|
27
|
-
if (typeof message.requestId !== "string" || !
|
|
69
|
+
if (typeof message.requestId !== "string" || !isNativeReadResult(message.result)) {
|
|
28
70
|
throw new Error("Invalid last-message response");
|
|
29
71
|
}
|
|
30
72
|
const pending = this.pending.get(message.requestId);
|
|
@@ -36,21 +78,32 @@ export class LastMessageRequests {
|
|
|
36
78
|
pending.resolve(message.result);
|
|
37
79
|
}
|
|
38
80
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
81
|
+
async last(socket: net.Socket, to: string, query: LastMessageQuery): Promise<LastMessageResult> {
|
|
82
|
+
const result = await this.request(socket, to, query);
|
|
83
|
+
return isLastMessageResult(result) ? result : { error: "Invalid last-message response" };
|
|
84
|
+
}
|
|
85
|
+
async receipt(socket: net.Socket, to: string, query: ReceiptQuery): Promise<ReceiptResult> {
|
|
86
|
+
const result = await this.request(socket, to, query);
|
|
87
|
+
return isReceiptResult(result) ? result : { error: "Invalid receipt response" };
|
|
88
|
+
}
|
|
89
|
+
private request(socket: net.Socket, to: string, query: unknown): Promise<NativeReadResult> {
|
|
90
|
+
if (!this.lastMessages) {
|
|
91
|
+
return Promise.reject(
|
|
92
|
+
new Error("This broker does not support last-message reads; reload after it exits."),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (!isNativeReadQuery(query)) {
|
|
46
96
|
return Promise.reject(new Error("Invalid last-message query"));
|
|
47
97
|
}
|
|
98
|
+
if ("messageId" in query && !this.receiptReads) {
|
|
99
|
+
return Promise.reject(new Error("publication_unavailable"));
|
|
100
|
+
}
|
|
48
101
|
return new Promise((resolve, reject) => {
|
|
49
102
|
const requestId = randomUUID();
|
|
50
103
|
const timer = setTimeout(() => {
|
|
51
104
|
this.pending.delete(requestId);
|
|
52
105
|
reject(new Error("Last-message read timeout"));
|
|
53
|
-
}, timeoutMs);
|
|
106
|
+
}, this.timeoutMs);
|
|
54
107
|
timer.unref();
|
|
55
108
|
this.pending.set(requestId, { resolve, reject, timer });
|
|
56
109
|
try {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type net from "node:net";
|
|
2
|
+
import { writeMessage } from "./framing.ts";
|
|
3
|
+
|
|
4
|
+
/** Parking negotiation and its single outstanding broker acknowledgement. */
|
|
5
|
+
export class ParkRequests {
|
|
6
|
+
supported = false;
|
|
7
|
+
private acknowledged?: (parked: boolean) => void;
|
|
8
|
+
settle(parked: boolean): void {
|
|
9
|
+
this.acknowledged?.(parked);
|
|
10
|
+
}
|
|
11
|
+
/** True once the broker retains later input; false when it cannot or the socket closed. */
|
|
12
|
+
request(socket: () => net.Socket, on: boolean): Promise<boolean> {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
this.acknowledged = on && this.supported ? resolve : undefined;
|
|
15
|
+
if (this.supported) {
|
|
16
|
+
writeMessage(socket(), { type: on ? "park" : "unpark" });
|
|
17
|
+
}
|
|
18
|
+
if (this.acknowledged === undefined) {
|
|
19
|
+
resolve(false);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -4,8 +4,9 @@ import { randomUUID } from "crypto";
|
|
|
4
4
|
import { writeMessage } from "./framing.ts";
|
|
5
5
|
import { BrokerConnection, connectBrokerSocket } from "./connection.ts";
|
|
6
6
|
import { SnapshotReader } from "./snapshot-reader.ts";
|
|
7
|
-
import { LastMessageRequests } from "./client-last.ts";
|
|
8
|
-
import {
|
|
7
|
+
import { LastMessageRequests, lastRequest } from "./client-last.ts";
|
|
8
|
+
import { ParkRequests } from "./client-park.ts";
|
|
9
|
+
import { isNativeReadResult } from "./last.ts";
|
|
9
10
|
import {
|
|
10
11
|
errorMessage,
|
|
11
12
|
isRecord,
|
|
@@ -18,16 +19,16 @@ import {
|
|
|
18
19
|
normalizeSessionInfo,
|
|
19
20
|
type SessionInfo,
|
|
20
21
|
type Message,
|
|
21
|
-
type Attachment,
|
|
22
22
|
type MessageDelivery,
|
|
23
|
-
type QueueMode,
|
|
24
|
-
type HumanMessageOrigin,
|
|
25
23
|
type TopicUpdate,
|
|
26
24
|
type TopicChange,
|
|
27
25
|
type SessionSnapshot,
|
|
28
26
|
type SendResult,
|
|
29
27
|
type LastMessageQuery,
|
|
30
28
|
type LastMessageResult,
|
|
29
|
+
type SendOptions,
|
|
30
|
+
type ReceiptQuery,
|
|
31
|
+
type ReceiptResult,
|
|
31
32
|
} from "../types.ts";
|
|
32
33
|
export type { SendResult } from "../types.ts";
|
|
33
34
|
|
|
@@ -43,20 +44,6 @@ export interface IntercomClientOptions {
|
|
|
43
44
|
readonly listTimeoutMs?: number;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
interface SendOptions {
|
|
47
|
-
readonly text: string;
|
|
48
|
-
readonly attachments?: readonly Attachment[];
|
|
49
|
-
readonly replyTo?: string;
|
|
50
|
-
readonly expectsReply?: boolean;
|
|
51
|
-
readonly delivery?: MessageDelivery;
|
|
52
|
-
readonly queueMode?: QueueMode;
|
|
53
|
-
readonly threadId?: string;
|
|
54
|
-
readonly passive?: boolean;
|
|
55
|
-
readonly messageId?: string;
|
|
56
|
-
readonly human?: HumanMessageOrigin;
|
|
57
|
-
readonly topic?: TopicUpdate;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
47
|
function toError(error: unknown): Error {
|
|
61
48
|
return error instanceof Error ? error : new Error(errorMessage(error));
|
|
62
49
|
}
|
|
@@ -78,8 +65,9 @@ export class IntercomClient extends EventEmitter {
|
|
|
78
65
|
private connection: BrokerConnection | null = null;
|
|
79
66
|
private _sessionId: string | null = null;
|
|
80
67
|
private _topicsSupported = false;
|
|
81
|
-
private
|
|
82
|
-
private
|
|
68
|
+
private readonly parking = new ParkRequests();
|
|
69
|
+
private guardedDeliverySupported = false;
|
|
70
|
+
private readonly lastRequests: LastMessageRequests;
|
|
83
71
|
private readonly pendingSends = new Map<
|
|
84
72
|
string,
|
|
85
73
|
{ readonly resolve: (r: SendResult) => void; readonly reject: (e: Readonly<Error>) => void }
|
|
@@ -101,6 +89,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
101
89
|
super();
|
|
102
90
|
this.sendTimeoutMs = options.sendTimeoutMs ?? DEFAULT_SEND_TIMEOUT_MS;
|
|
103
91
|
this.listTimeoutMs = options.listTimeoutMs ?? DEFAULT_LIST_TIMEOUT_MS;
|
|
92
|
+
this.lastRequests = new LastMessageRequests(this.listTimeoutMs);
|
|
104
93
|
}
|
|
105
94
|
|
|
106
95
|
private failPending(error: Readonly<Error>): void {
|
|
@@ -122,6 +111,9 @@ export class IntercomClient extends EventEmitter {
|
|
|
122
111
|
get supportsTopics(): boolean {
|
|
123
112
|
return this.isConnected() && this._topicsSupported;
|
|
124
113
|
}
|
|
114
|
+
get supportsGuardedDelivery(): boolean {
|
|
115
|
+
return this.isConnected() && this.guardedDeliverySupported;
|
|
116
|
+
}
|
|
125
117
|
isConnected(): boolean {
|
|
126
118
|
return (
|
|
127
119
|
(this._sessionId ?? "").length > 0 &&
|
|
@@ -164,7 +156,8 @@ export class IntercomClient extends EventEmitter {
|
|
|
164
156
|
this.connection = null;
|
|
165
157
|
this._sessionId = null;
|
|
166
158
|
this._topicsSupported = false;
|
|
167
|
-
this.
|
|
159
|
+
this.parking.settle(false);
|
|
160
|
+
this.guardedDeliverySupported = false;
|
|
168
161
|
this.disconnecting = false;
|
|
169
162
|
this.failPending(error);
|
|
170
163
|
if (established && !expected) {
|
|
@@ -181,6 +174,9 @@ export class IntercomClient extends EventEmitter {
|
|
|
181
174
|
requestedId,
|
|
182
175
|
topicFrames: true,
|
|
183
176
|
lastMessages: true,
|
|
177
|
+
parking: true,
|
|
178
|
+
receiptReads: 1,
|
|
179
|
+
guardedDelivery: 1,
|
|
184
180
|
});
|
|
185
181
|
} catch (error) {
|
|
186
182
|
if (this.connection === connection) {
|
|
@@ -245,11 +241,15 @@ export class IntercomClient extends EventEmitter {
|
|
|
245
241
|
case "delivery_failed":
|
|
246
242
|
this.receiveDelivery(brokerMessage);
|
|
247
243
|
break;
|
|
244
|
+
case "parked":
|
|
245
|
+
this.parking.settle(true);
|
|
246
|
+
break;
|
|
247
|
+
case "wake":
|
|
248
248
|
case "session_left":
|
|
249
249
|
if (typeof brokerMessage.sessionId !== "string") {
|
|
250
|
-
throw new Error(
|
|
250
|
+
throw new Error(`Invalid ${type} message`);
|
|
251
251
|
}
|
|
252
|
-
this.emit(
|
|
252
|
+
this.emit(type, brokerMessage.sessionId);
|
|
253
253
|
break;
|
|
254
254
|
default:
|
|
255
255
|
throw new Error(`Unknown broker message type: ${type}`);
|
|
@@ -265,18 +265,18 @@ export class IntercomClient extends EventEmitter {
|
|
|
265
265
|
}
|
|
266
266
|
this._sessionId = msg.sessionId;
|
|
267
267
|
this._topicsSupported = msg.topicsSupported === true && msg.topicFrames === true;
|
|
268
|
-
this.
|
|
268
|
+
this.lastRequests.lastMessages = msg.lastMessages === true;
|
|
269
|
+
this.parking.supported = msg.parking === true;
|
|
270
|
+
this.lastRequests.receiptReads = msg.receiptReads === 1;
|
|
271
|
+
this.guardedDeliverySupported = msg.guardedDelivery === 1;
|
|
269
272
|
this.connection?.confirmRegistration();
|
|
270
273
|
this.emit("_registered", { type: "registered", sessionId: msg.sessionId });
|
|
271
274
|
}
|
|
272
275
|
|
|
273
276
|
private receiveLastRequest(msg: UnknownRecord): void {
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
if (!this.emit("last_request", { requestId: msg.requestId, from, query: msg.query })) {
|
|
279
|
-
this.respondLast(msg.requestId, { error: "Recipient has no last-message reader." });
|
|
277
|
+
const request = lastRequest(msg);
|
|
278
|
+
if (!this.emit("last_request", request)) {
|
|
279
|
+
this.respondLast(request.requestId, { error: "Recipient has no last-message reader." });
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
|
@@ -337,6 +337,10 @@ export class IntercomClient extends EventEmitter {
|
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
park(on = true): Promise<boolean> {
|
|
341
|
+
return this.parking.request(() => this.requireActiveSocket(), on);
|
|
342
|
+
}
|
|
343
|
+
|
|
340
344
|
async disconnect(): Promise<void> {
|
|
341
345
|
const connection = this.connection;
|
|
342
346
|
if (!connection) {
|
|
@@ -351,20 +355,15 @@ export class IntercomClient extends EventEmitter {
|
|
|
351
355
|
return this.requestSessions().then((snapshot) => [...snapshot.sessions]);
|
|
352
356
|
}
|
|
353
357
|
|
|
354
|
-
last(to: string, query: LastMessageQuery = {}): Promise<LastMessageResult> {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
return this.lastRequests.request(socket, to, query, this.listTimeoutMs);
|
|
361
|
-
} catch (error) {
|
|
362
|
-
return Promise.reject(toError(error));
|
|
363
|
-
}
|
|
358
|
+
async last(to: string, query: LastMessageQuery = {}): Promise<LastMessageResult> {
|
|
359
|
+
return await this.lastRequests.last(this.requireActiveSocket(), to, query);
|
|
360
|
+
}
|
|
361
|
+
async receipt(to: string, query: ReceiptQuery): Promise<ReceiptResult> {
|
|
362
|
+
return await this.lastRequests.receipt(this.requireActiveSocket(), to, query);
|
|
364
363
|
}
|
|
365
364
|
|
|
366
365
|
respondLast(requestId: string, result: unknown): void {
|
|
367
|
-
if (!
|
|
366
|
+
if (!isNativeReadResult(result)) {
|
|
368
367
|
throw new Error("Invalid last-message result");
|
|
369
368
|
}
|
|
370
369
|
writeMessage(this.requireActiveSocket(), { type: "last_response", requestId, result });
|
|
@@ -410,7 +409,9 @@ export class IntercomClient extends EventEmitter {
|
|
|
410
409
|
const timeout = setTimeout(() => {
|
|
411
410
|
if (this.pendingLists.has(requestId)) {
|
|
412
411
|
this.pendingLists.delete(requestId);
|
|
413
|
-
wrappedReject(
|
|
412
|
+
wrappedReject(
|
|
413
|
+
Object.assign(new Error("List sessions timeout"), { code: "list_timeout" }),
|
|
414
|
+
);
|
|
414
415
|
}
|
|
415
416
|
}, this.listTimeoutMs);
|
|
416
417
|
timeout.unref();
|
|
@@ -434,20 +435,33 @@ export class IntercomClient extends EventEmitter {
|
|
|
434
435
|
});
|
|
435
436
|
}
|
|
436
437
|
|
|
438
|
+
private requireSendSocket(options: SendOptions): net.Socket {
|
|
439
|
+
const socket = this.requireActiveSocket();
|
|
440
|
+
if (options.guard && !this.guardedDeliverySupported) {
|
|
441
|
+
throw new Error("This broker does not support guarded delivery; no message sent.");
|
|
442
|
+
}
|
|
443
|
+
return socket;
|
|
444
|
+
}
|
|
445
|
+
|
|
437
446
|
send(to: string, options: SendOptions): Promise<SendResult> {
|
|
438
447
|
let socket: net.Socket;
|
|
439
448
|
try {
|
|
440
|
-
socket = this.
|
|
449
|
+
socket = this.requireSendSocket(options);
|
|
441
450
|
} catch (error) {
|
|
442
451
|
return Promise.reject(toError(error));
|
|
443
452
|
}
|
|
444
453
|
|
|
445
|
-
const messageId = options.messageId ?? randomUUID();
|
|
454
|
+
const messageId = options.messageId ?? (options.guard ? "" : randomUUID());
|
|
455
|
+
if (this.pendingSends.has(messageId)) {
|
|
456
|
+
return Promise.reject(new Error("Message ID already has a pending send"));
|
|
457
|
+
}
|
|
446
458
|
const message: Message = {
|
|
447
459
|
id: messageId,
|
|
448
460
|
timestamp: Date.now(),
|
|
449
461
|
replyTo: options.replyTo,
|
|
450
462
|
expectsReply: options.expectsReply,
|
|
463
|
+
guard: options.guard,
|
|
464
|
+
replyHint: options.replyHint,
|
|
451
465
|
...(options.human ? { human: options.human } : {}),
|
|
452
466
|
...(options.topic ? { topic: options.topic } : {}),
|
|
453
467
|
delivery: messageDelivery(options),
|
|
@@ -460,6 +474,9 @@ export class IntercomClient extends EventEmitter {
|
|
|
460
474
|
},
|
|
461
475
|
};
|
|
462
476
|
|
|
477
|
+
if ((options.guard || options.replyHint) && normalizeMessage(message) === null) {
|
|
478
|
+
return Promise.reject(new Error("Invalid message format"));
|
|
479
|
+
}
|
|
463
480
|
return new Promise((resolve, reject) => {
|
|
464
481
|
const wrappedResolve = (result: SendResult) => {
|
|
465
482
|
clearTimeout(timeout);
|
|
@@ -493,6 +510,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
493
510
|
}
|
|
494
511
|
|
|
495
512
|
updatePresence(updates: {
|
|
513
|
+
readonly applicationGuard?: string | null;
|
|
496
514
|
readonly name?: string;
|
|
497
515
|
readonly status?: string;
|
|
498
516
|
readonly model?: string;
|