@fitchmultz/pi-subagents 0.50.0 → 0.51.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 +51 -2
- package/README.md +36 -5
- package/dist/agent/contracts.js +6 -24
- package/dist/agent/host.js +3 -16
- package/dist/agent/index.js +207 -173
- 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 +346 -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/process.js +198 -25
- package/dist/agent/receipts.js +92 -20
- package/dist/agent/runner.js +125 -20
- package/dist/agent/shell.js +2 -1
- package/dist/agent/storage.js +29 -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 +27 -21
- package/dist/pi-intercom/broker/client-last.d.ts +13 -0
- package/dist/pi-intercom/broker/client-last.js +17 -5
- package/dist/pi-intercom/broker/client.d.ts +60 -0
- package/dist/pi-intercom/broker/client.js +47 -16
- 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 +26 -0
- package/dist/pi-intercom/broker/last.js +57 -32
- 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 +19 -0
- 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 +10 -2
- 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 +12 -38
- package/dist/pi-intercom/index.js +3 -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/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 +2 -0
- package/docs/development.md +40 -1
- package/docs/intercom-bridge.md +9 -4
- package/docs/intercom.md +226 -6
- package/docs/performance.md +87 -11
- package/docs/reference.md +157 -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/contracts.ts +8 -34
- package/src/agent/host.ts +19 -20
- package/src/agent/index.ts +224 -190
- 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 +405 -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/process.ts +240 -34
- package/src/agent/receipts.ts +114 -24
- package/src/agent/runner.ts +125 -27
- package/src/agent/shell.ts +2 -1
- package/src/agent/storage.ts +27 -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 +27 -23
- package/src/pi-intercom/broker/client-last.ts +25 -12
- package/src/pi-intercom/broker/client.ts +55 -33
- 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 +86 -33
- 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 +25 -0
- package/src/pi-intercom/connection.ts +21 -10
- 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 +18 -49
- package/src/pi-intercom/index.ts +3 -0
- package/src/pi-intercom/last-message.ts +10 -19
- 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/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,9 +7,10 @@ import { getBrokerRuntimeDir, prepareBrokerSocketPath } from "./paths.js";
|
|
|
7
7
|
import { brokerPidRecord } from "./pid.js";
|
|
8
8
|
import { assertBrokerCanStart } from "./spawn.js";
|
|
9
9
|
import { LastMessageRelay } from "./last.js";
|
|
10
|
-
import {
|
|
11
|
-
import { acceptBrokerConnection } from "./server-connection.js";
|
|
12
|
-
import {
|
|
10
|
+
import { canSeeSession, findSessions } from "./targets.js";
|
|
11
|
+
import { acceptBrokerConnection, registeredSessionInfo } from "./server-connection.js";
|
|
12
|
+
import { guardedDeliveryError } from "./guard.js";
|
|
13
|
+
import { compactTopicMessage, normalizeMessage, normalizeSessionInfo, } from "../types.js";
|
|
13
14
|
import { errorMessage, isRecord } from "../../shared/unknown.js";
|
|
14
15
|
import { messageSender, snapshotFrames, TopicSnapshot, publicationMessage, deliveryAcknowledgement, shouldReplaceDelivery, } from "./snapshots.js";
|
|
15
16
|
const PID_PATH = join(getBrokerRuntimeDir(), "broker.pid");
|
|
@@ -106,15 +107,8 @@ class IntercomBroker {
|
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
register(socket, msg, setId) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
assertMainParent(msg.session, this.sessions);
|
|
113
|
-
const requestedId = typeof msg.requestedId === "string" && /^[a-zA-Z0-9_-]{8,80}$/.test(msg.requestedId)
|
|
114
|
-
? msg.requestedId
|
|
115
|
-
: undefined;
|
|
116
|
-
const id = requestedId !== undefined && !this.sessions.has(requestedId) ? requestedId : randomUUID();
|
|
117
|
-
const info = { ...msg.session, id, lastSeen: msg.session.lastSeen ?? Date.now() };
|
|
110
|
+
const info = registeredSessionInfo(msg, this.sessions);
|
|
111
|
+
const id = info.id;
|
|
118
112
|
snapshotFrames(randomUUID(), [info], true);
|
|
119
113
|
const topicFrames = msg.topicFrames === true;
|
|
120
114
|
this.sessions.set(id, {
|
|
@@ -123,6 +117,7 @@ class IntercomBroker {
|
|
|
123
117
|
topics: new TopicSnapshot(info),
|
|
124
118
|
topicFrames,
|
|
125
119
|
lastMessages: msg.lastMessages === true,
|
|
120
|
+
receiptReads: msg.receiptReads === 1,
|
|
126
121
|
});
|
|
127
122
|
setId(id);
|
|
128
123
|
if (this.shutdownTimer) {
|
|
@@ -133,6 +128,8 @@ class IntercomBroker {
|
|
|
133
128
|
type: "registered",
|
|
134
129
|
sessionId: id,
|
|
135
130
|
lastMessages: true,
|
|
131
|
+
receiptReads: 1,
|
|
132
|
+
...(msg.guardedDelivery === 1 ? { guardedDelivery: 1 } : {}),
|
|
136
133
|
...(topicFrames ? { topicsSupported: true, topicFrames: true } : {}),
|
|
137
134
|
});
|
|
138
135
|
}
|
|
@@ -240,6 +237,15 @@ class IntercomBroker {
|
|
|
240
237
|
const targets = findSessions(this.sessions, msg.to, currentId);
|
|
241
238
|
const target = targets[0];
|
|
242
239
|
if (targets.length === 1) {
|
|
240
|
+
const guardError = guardedDeliveryError(this.sessions, msg.to, message);
|
|
241
|
+
if (guardError !== null) {
|
|
242
|
+
writeMessage(socket, {
|
|
243
|
+
type: "delivery_failed",
|
|
244
|
+
messageId: message.id,
|
|
245
|
+
reason: guardError,
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
243
249
|
const fromSession = this.sessions.get(currentId);
|
|
244
250
|
if (!fromSession) {
|
|
245
251
|
writeMessage(socket, {
|
|
@@ -280,11 +286,15 @@ class IntercomBroker {
|
|
|
280
286
|
"lastIntercomActivity",
|
|
281
287
|
"subscriptions",
|
|
282
288
|
"topics",
|
|
289
|
+
"applicationGuard",
|
|
283
290
|
]) {
|
|
284
291
|
if (msg[key] !== undefined) {
|
|
285
292
|
update[key] = msg[key];
|
|
286
293
|
}
|
|
287
294
|
}
|
|
295
|
+
if (msg.applicationGuard === null) {
|
|
296
|
+
delete update.applicationGuard;
|
|
297
|
+
}
|
|
288
298
|
const nextInfo = normalizeSessionInfo(update);
|
|
289
299
|
if (!nextInfo) {
|
|
290
300
|
throw new Error("Invalid presence update");
|
|
@@ -297,15 +307,7 @@ class IntercomBroker {
|
|
|
297
307
|
}
|
|
298
308
|
sendToSession(fromId, toId, from, message) {
|
|
299
309
|
const target = this.sessions.get(toId);
|
|
300
|
-
if (
|
|
301
|
-
return {
|
|
302
|
-
id: message.id,
|
|
303
|
-
accepted: false,
|
|
304
|
-
delivered: false,
|
|
305
|
-
reason: "Recipient disconnected before delivery",
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
if (shouldReplaceDelivery(message, target.info)) {
|
|
310
|
+
if (target && shouldReplaceDelivery(message, target.info)) {
|
|
309
311
|
return this.queueReplaceDelivery(fromId, toId, from, message);
|
|
310
312
|
}
|
|
311
313
|
const reason = this.deliverMessage(toId, from, message);
|
|
@@ -373,6 +375,10 @@ class IntercomBroker {
|
|
|
373
375
|
})?.message ?? null);
|
|
374
376
|
}
|
|
375
377
|
deliverMessage(toId, from, message) {
|
|
378
|
+
const guardError = guardedDeliveryError(this.sessions, toId, message);
|
|
379
|
+
if (guardError !== null) {
|
|
380
|
+
return guardError;
|
|
381
|
+
}
|
|
376
382
|
const target = this.sessions.get(toId);
|
|
377
383
|
if (!target ||
|
|
378
384
|
target.socket.destroyed ||
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type net from "node:net";
|
|
2
|
+
import { type UnknownRecord } from "../../shared/unknown.ts";
|
|
3
|
+
import type { LastMessageQuery, LastMessageResult, ReceiptQuery, ReceiptResult } from "../types.ts";
|
|
4
|
+
export declare class LastMessageRequests {
|
|
5
|
+
private readonly timeoutMs;
|
|
6
|
+
constructor(timeoutMs: number);
|
|
7
|
+
private readonly pending;
|
|
8
|
+
fail(error: Readonly<Error>): void;
|
|
9
|
+
receive(message: UnknownRecord): void;
|
|
10
|
+
last(socket: net.Socket, to: string, query: LastMessageQuery): Promise<LastMessageResult>;
|
|
11
|
+
receipt(socket: net.Socket, to: string, query: ReceiptQuery): Promise<ReceiptResult>;
|
|
12
|
+
private request;
|
|
13
|
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { errorMessage } from "../../shared/unknown.js";
|
|
3
|
-
import {
|
|
3
|
+
import { isNativeReadQuery, isNativeReadResult, isLastMessageResult, isReceiptResult, } from "./last.js";
|
|
4
4
|
import { writeMessage } from "./framing.js";
|
|
5
5
|
export class LastMessageRequests {
|
|
6
|
+
timeoutMs;
|
|
7
|
+
constructor(timeoutMs) {
|
|
8
|
+
this.timeoutMs = timeoutMs;
|
|
9
|
+
}
|
|
6
10
|
pending = new Map();
|
|
7
11
|
fail(error) {
|
|
8
12
|
for (const pending of this.pending.values()) {
|
|
@@ -12,7 +16,7 @@ export class LastMessageRequests {
|
|
|
12
16
|
this.pending.clear();
|
|
13
17
|
}
|
|
14
18
|
receive(message) {
|
|
15
|
-
if (typeof message.requestId !== "string" || !
|
|
19
|
+
if (typeof message.requestId !== "string" || !isNativeReadResult(message.result)) {
|
|
16
20
|
throw new Error("Invalid last-message response");
|
|
17
21
|
}
|
|
18
22
|
const pending = this.pending.get(message.requestId);
|
|
@@ -23,8 +27,16 @@ export class LastMessageRequests {
|
|
|
23
27
|
this.pending.delete(message.requestId);
|
|
24
28
|
pending.resolve(message.result);
|
|
25
29
|
}
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
async last(socket, to, query) {
|
|
31
|
+
const result = await this.request(socket, to, query);
|
|
32
|
+
return isLastMessageResult(result) ? result : { error: "Invalid last-message response" };
|
|
33
|
+
}
|
|
34
|
+
async receipt(socket, to, query) {
|
|
35
|
+
const result = await this.request(socket, to, query);
|
|
36
|
+
return isReceiptResult(result) ? result : { error: "Invalid receipt response" };
|
|
37
|
+
}
|
|
38
|
+
request(socket, to, query) {
|
|
39
|
+
if (!isNativeReadQuery(query)) {
|
|
28
40
|
return Promise.reject(new Error("Invalid last-message query"));
|
|
29
41
|
}
|
|
30
42
|
return new Promise((resolve, reject) => {
|
|
@@ -32,7 +44,7 @@ export class LastMessageRequests {
|
|
|
32
44
|
const timer = setTimeout(() => {
|
|
33
45
|
this.pending.delete(requestId);
|
|
34
46
|
reject(new Error("Last-message read timeout"));
|
|
35
|
-
}, timeoutMs);
|
|
47
|
+
}, this.timeoutMs);
|
|
36
48
|
timer.unref();
|
|
37
49
|
this.pending.set(requestId, { resolve, reject, timer });
|
|
38
50
|
try {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import { type SessionInfo, type TopicUpdate, type TopicChange, type SessionSnapshot, type SendResult, type LastMessageQuery, type LastMessageResult, type SendOptions, type ReceiptQuery, type ReceiptResult } from "../types.ts";
|
|
3
|
+
export type { SendResult } from "../types.ts";
|
|
4
|
+
export interface IntercomClientOptions {
|
|
5
|
+
/** Timeout (ms) for the broker to acknowledge message delivery. */
|
|
6
|
+
readonly sendTimeoutMs?: number;
|
|
7
|
+
/** Timeout (ms) for a session list response. */
|
|
8
|
+
readonly listTimeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare class IntercomClient extends EventEmitter {
|
|
11
|
+
private connection;
|
|
12
|
+
private _sessionId;
|
|
13
|
+
private _topicsSupported;
|
|
14
|
+
private lastMessagesSupported;
|
|
15
|
+
private receiptReadsSupported;
|
|
16
|
+
private guardedDeliverySupported;
|
|
17
|
+
private readonly lastRequests;
|
|
18
|
+
private readonly pendingSends;
|
|
19
|
+
private readonly pendingLists;
|
|
20
|
+
private connecting;
|
|
21
|
+
private disconnecting;
|
|
22
|
+
private readonly sendTimeoutMs;
|
|
23
|
+
private readonly listTimeoutMs;
|
|
24
|
+
constructor(options?: IntercomClientOptions);
|
|
25
|
+
private failPending;
|
|
26
|
+
get sessionId(): string | null;
|
|
27
|
+
get supportsTopics(): boolean;
|
|
28
|
+
get supportsGuardedDelivery(): boolean;
|
|
29
|
+
isConnected(): boolean;
|
|
30
|
+
private requireActiveSocket;
|
|
31
|
+
connect(session: Omit<SessionInfo, "id">, requestedId?: string): Promise<void>;
|
|
32
|
+
private restoreTopics;
|
|
33
|
+
private handleBrokerMessage;
|
|
34
|
+
private receiveRegistration;
|
|
35
|
+
private receiveLastRequest;
|
|
36
|
+
private receiveMessage;
|
|
37
|
+
private receiveDelivery;
|
|
38
|
+
private receiveSessions;
|
|
39
|
+
disconnect(): Promise<void>;
|
|
40
|
+
listSessions(): Promise<SessionInfo[]>;
|
|
41
|
+
last(to: string, query?: LastMessageQuery): Promise<LastMessageResult>;
|
|
42
|
+
receipt(to: string, query: ReceiptQuery): Promise<ReceiptResult>;
|
|
43
|
+
private readSocket;
|
|
44
|
+
respondLast(requestId: string, result: unknown): void;
|
|
45
|
+
updateTopics(change: TopicChange, onAccepted?: (snapshot: SessionSnapshot) => void): Promise<SessionSnapshot>;
|
|
46
|
+
private requestSessions;
|
|
47
|
+
private requireSendSocket;
|
|
48
|
+
send(to: string, options: SendOptions): Promise<SendResult>;
|
|
49
|
+
updatePresence(updates: {
|
|
50
|
+
readonly applicationGuard?: string | null;
|
|
51
|
+
readonly name?: string;
|
|
52
|
+
readonly status?: string;
|
|
53
|
+
readonly model?: string;
|
|
54
|
+
readonly pendingAsks?: number;
|
|
55
|
+
readonly acceptsAsks?: boolean;
|
|
56
|
+
readonly lastIntercomActivity?: number;
|
|
57
|
+
readonly subscriptions?: SessionInfo["subscriptions"];
|
|
58
|
+
readonly topics?: readonly TopicUpdate[];
|
|
59
|
+
}): void;
|
|
60
|
+
}
|
|
@@ -4,7 +4,7 @@ import { writeMessage } from "./framing.js";
|
|
|
4
4
|
import { BrokerConnection, connectBrokerSocket } from "./connection.js";
|
|
5
5
|
import { SnapshotReader } from "./snapshot-reader.js";
|
|
6
6
|
import { LastMessageRequests } from "./client-last.js";
|
|
7
|
-
import {
|
|
7
|
+
import { isNativeReadQuery, isNativeReadResult } from "./last.js";
|
|
8
8
|
import { errorMessage, isRecord, isUnknownArray, } from "../../shared/unknown.js";
|
|
9
9
|
import { compactTopicMessage, normalizeMessage, normalizeSessionInfo, } from "../types.js";
|
|
10
10
|
/** Default delivery-ack timeout for `send` (broker acknowledges quickly). */
|
|
@@ -31,7 +31,9 @@ export class IntercomClient extends EventEmitter {
|
|
|
31
31
|
_sessionId = null;
|
|
32
32
|
_topicsSupported = false;
|
|
33
33
|
lastMessagesSupported = false;
|
|
34
|
-
|
|
34
|
+
receiptReadsSupported = false;
|
|
35
|
+
guardedDeliverySupported = false;
|
|
36
|
+
lastRequests;
|
|
35
37
|
pendingSends = new Map();
|
|
36
38
|
pendingLists = new Map();
|
|
37
39
|
connecting = false;
|
|
@@ -42,6 +44,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
42
44
|
super();
|
|
43
45
|
this.sendTimeoutMs = options.sendTimeoutMs ?? DEFAULT_SEND_TIMEOUT_MS;
|
|
44
46
|
this.listTimeoutMs = options.listTimeoutMs ?? DEFAULT_LIST_TIMEOUT_MS;
|
|
47
|
+
this.lastRequests = new LastMessageRequests(this.listTimeoutMs);
|
|
45
48
|
}
|
|
46
49
|
failPending(error) {
|
|
47
50
|
this.lastRequests.fail(error);
|
|
@@ -60,6 +63,9 @@ export class IntercomClient extends EventEmitter {
|
|
|
60
63
|
get supportsTopics() {
|
|
61
64
|
return this.isConnected() && this._topicsSupported;
|
|
62
65
|
}
|
|
66
|
+
get supportsGuardedDelivery() {
|
|
67
|
+
return this.isConnected() && this.guardedDeliverySupported;
|
|
68
|
+
}
|
|
63
69
|
isConnected() {
|
|
64
70
|
return ((this._sessionId ?? "").length > 0 &&
|
|
65
71
|
!this.disconnecting &&
|
|
@@ -99,6 +105,8 @@ export class IntercomClient extends EventEmitter {
|
|
|
99
105
|
this._sessionId = null;
|
|
100
106
|
this._topicsSupported = false;
|
|
101
107
|
this.lastMessagesSupported = false;
|
|
108
|
+
this.receiptReadsSupported = false;
|
|
109
|
+
this.guardedDeliverySupported = false;
|
|
102
110
|
this.disconnecting = false;
|
|
103
111
|
this.failPending(error);
|
|
104
112
|
if (established && !expected) {
|
|
@@ -115,6 +123,8 @@ export class IntercomClient extends EventEmitter {
|
|
|
115
123
|
requestedId,
|
|
116
124
|
topicFrames: true,
|
|
117
125
|
lastMessages: true,
|
|
126
|
+
receiptReads: 1,
|
|
127
|
+
guardedDelivery: 1,
|
|
118
128
|
});
|
|
119
129
|
}
|
|
120
130
|
catch (error) {
|
|
@@ -193,12 +203,14 @@ export class IntercomClient extends EventEmitter {
|
|
|
193
203
|
this._sessionId = msg.sessionId;
|
|
194
204
|
this._topicsSupported = msg.topicsSupported === true && msg.topicFrames === true;
|
|
195
205
|
this.lastMessagesSupported = msg.lastMessages === true;
|
|
206
|
+
this.receiptReadsSupported = msg.receiptReads === 1;
|
|
207
|
+
this.guardedDeliverySupported = msg.guardedDelivery === 1;
|
|
196
208
|
this.connection?.confirmRegistration();
|
|
197
209
|
this.emit("_registered", { type: "registered", sessionId: msg.sessionId });
|
|
198
210
|
}
|
|
199
211
|
receiveLastRequest(msg) {
|
|
200
212
|
const from = normalizeSessionInfo(msg.from);
|
|
201
|
-
if (typeof msg.requestId !== "string" || !from || !
|
|
213
|
+
if (typeof msg.requestId !== "string" || !from || !isNativeReadQuery(msg.query)) {
|
|
202
214
|
throw new Error("Invalid last-message request");
|
|
203
215
|
}
|
|
204
216
|
if (!this.emit("last_request", { requestId: msg.requestId, from, query: msg.query })) {
|
|
@@ -269,20 +281,24 @@ export class IntercomClient extends EventEmitter {
|
|
|
269
281
|
listSessions() {
|
|
270
282
|
return this.requestSessions().then((snapshot) => [...snapshot.sessions]);
|
|
271
283
|
}
|
|
272
|
-
last(to, query = {}) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
284
|
+
async last(to, query = {}) {
|
|
285
|
+
return await this.lastRequests.last(this.readSocket(query), to, query);
|
|
286
|
+
}
|
|
287
|
+
async receipt(to, query) {
|
|
288
|
+
return await this.lastRequests.receipt(this.readSocket(query), to, query);
|
|
289
|
+
}
|
|
290
|
+
readSocket(query) {
|
|
291
|
+
const socket = this.requireActiveSocket();
|
|
292
|
+
if (!this.lastMessagesSupported) {
|
|
293
|
+
throw new Error("This broker does not support last-message reads; reload after it exits.");
|
|
279
294
|
}
|
|
280
|
-
|
|
281
|
-
|
|
295
|
+
if ("messageId" in query && !this.receiptReadsSupported) {
|
|
296
|
+
throw new Error("publication_unavailable");
|
|
282
297
|
}
|
|
298
|
+
return socket;
|
|
283
299
|
}
|
|
284
300
|
respondLast(requestId, result) {
|
|
285
|
-
if (!
|
|
301
|
+
if (!isNativeReadResult(result)) {
|
|
286
302
|
throw new Error("Invalid last-message result");
|
|
287
303
|
}
|
|
288
304
|
writeMessage(this.requireActiveSocket(), { type: "last_response", requestId, result });
|
|
@@ -321,7 +337,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
321
337
|
const timeout = setTimeout(() => {
|
|
322
338
|
if (this.pendingLists.has(requestId)) {
|
|
323
339
|
this.pendingLists.delete(requestId);
|
|
324
|
-
wrappedReject(new Error("List sessions timeout"));
|
|
340
|
+
wrappedReject(Object.assign(new Error("List sessions timeout"), { code: "list_timeout" }));
|
|
325
341
|
}
|
|
326
342
|
}, this.listTimeoutMs);
|
|
327
343
|
timeout.unref();
|
|
@@ -345,20 +361,32 @@ export class IntercomClient extends EventEmitter {
|
|
|
345
361
|
}
|
|
346
362
|
});
|
|
347
363
|
}
|
|
364
|
+
requireSendSocket(options) {
|
|
365
|
+
const socket = this.requireActiveSocket();
|
|
366
|
+
if (options.guard && !this.guardedDeliverySupported) {
|
|
367
|
+
throw new Error("This broker does not support guarded delivery; no message sent.");
|
|
368
|
+
}
|
|
369
|
+
return socket;
|
|
370
|
+
}
|
|
348
371
|
send(to, options) {
|
|
349
372
|
let socket;
|
|
350
373
|
try {
|
|
351
|
-
socket = this.
|
|
374
|
+
socket = this.requireSendSocket(options);
|
|
352
375
|
}
|
|
353
376
|
catch (error) {
|
|
354
377
|
return Promise.reject(toError(error));
|
|
355
378
|
}
|
|
356
|
-
const messageId = options.messageId ?? randomUUID();
|
|
379
|
+
const messageId = options.messageId ?? (options.guard ? "" : randomUUID());
|
|
380
|
+
if (this.pendingSends.has(messageId)) {
|
|
381
|
+
return Promise.reject(new Error("Message ID already has a pending send"));
|
|
382
|
+
}
|
|
357
383
|
const message = {
|
|
358
384
|
id: messageId,
|
|
359
385
|
timestamp: Date.now(),
|
|
360
386
|
replyTo: options.replyTo,
|
|
361
387
|
expectsReply: options.expectsReply,
|
|
388
|
+
guard: options.guard,
|
|
389
|
+
replyHint: options.replyHint,
|
|
362
390
|
...(options.human ? { human: options.human } : {}),
|
|
363
391
|
...(options.topic ? { topic: options.topic } : {}),
|
|
364
392
|
delivery: messageDelivery(options),
|
|
@@ -370,6 +398,9 @@ export class IntercomClient extends EventEmitter {
|
|
|
370
398
|
attachments: options.attachments,
|
|
371
399
|
},
|
|
372
400
|
};
|
|
401
|
+
if ((options.guard || options.replyHint) && normalizeMessage(message) === null) {
|
|
402
|
+
return Promise.reject(new Error("Invalid message format"));
|
|
403
|
+
}
|
|
373
404
|
return new Promise((resolve, reject) => {
|
|
374
405
|
const wrappedResolve = (result) => {
|
|
375
406
|
clearTimeout(timeout);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
export declare function connectBrokerSocket(): Promise<net.Socket>;
|
|
3
|
+
interface ConnectionHandlers {
|
|
4
|
+
readonly message: (message: unknown) => void;
|
|
5
|
+
readonly error: (error: Readonly<Error>) => void;
|
|
6
|
+
readonly closed: (error: Readonly<Error>, expected: boolean) => void;
|
|
7
|
+
}
|
|
8
|
+
/** One socket owns registration, read/error listeners, and graceful shutdown. */
|
|
9
|
+
export declare class BrokerConnection {
|
|
10
|
+
private registered;
|
|
11
|
+
private closing;
|
|
12
|
+
private failure;
|
|
13
|
+
private registration;
|
|
14
|
+
private registrationTimer;
|
|
15
|
+
private readonly reader;
|
|
16
|
+
readonly socket: net.Socket;
|
|
17
|
+
private readonly handlers;
|
|
18
|
+
constructor(socket: net.Socket, handlers: ConnectionHandlers);
|
|
19
|
+
start(registration: unknown): Promise<void>;
|
|
20
|
+
confirmRegistration(): void;
|
|
21
|
+
private clearRegistration;
|
|
22
|
+
private failRegistration;
|
|
23
|
+
private protocolError;
|
|
24
|
+
private readonly onError;
|
|
25
|
+
private readonly onClose;
|
|
26
|
+
isActive(): boolean;
|
|
27
|
+
disconnect(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Socket } from "net";
|
|
2
|
+
export declare const MAX_FRAME_SIZE_BYTES: number;
|
|
3
|
+
export declare function intercomMessageSizeBytes(msg: unknown): number;
|
|
4
|
+
export declare function validateIntercomMessageSize(msg: unknown): Error | null;
|
|
5
|
+
/**
|
|
6
|
+
* Write a length-prefixed message to a socket.
|
|
7
|
+
* Format: 4-byte big-endian length + JSON payload
|
|
8
|
+
*/
|
|
9
|
+
export declare function writeMessage(socket: Socket, msg: unknown): void;
|
|
10
|
+
/** Write serialized JSON; false means Node accepted it but the caller should wait for drain. */
|
|
11
|
+
export declare function writeFrame(socket: Socket, json: string, limit?: number): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Create a message reader that handles partial reads.
|
|
14
|
+
* Calls onMessage for each complete message received.
|
|
15
|
+
* Protocol or handler errors are reported to onError so the caller can close the socket.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createMessageReader(onMessage: (msg: unknown) => void, onError: (error: Readonly<Error>) => void, limit?: number): (data: Buffer) => void;
|
|
@@ -14,15 +14,18 @@ export function validateIntercomMessageSize(msg) {
|
|
|
14
14
|
* Format: 4-byte big-endian length + JSON payload
|
|
15
15
|
*/
|
|
16
16
|
export function writeMessage(socket, msg) {
|
|
17
|
-
|
|
17
|
+
writeFrame(socket, JSON.stringify(msg));
|
|
18
|
+
}
|
|
19
|
+
/** Write serialized JSON; false means Node accepted it but the caller should wait for drain. */
|
|
20
|
+
export function writeFrame(socket, json, limit = MAX_FRAME_SIZE_BYTES) {
|
|
18
21
|
const size = Buffer.byteLength(json, "utf-8");
|
|
19
|
-
if (size >
|
|
20
|
-
throw new Error(`Intercom message too large (${size} bytes; max ${
|
|
22
|
+
if (size > limit) {
|
|
23
|
+
throw new Error(`Intercom message too large (${size} bytes; max ${limit})`);
|
|
21
24
|
}
|
|
22
25
|
const payload = Buffer.from(json, "utf-8");
|
|
23
26
|
const header = Buffer.alloc(4);
|
|
24
27
|
header.writeUInt32BE(payload.length, 0);
|
|
25
|
-
socket.write(Buffer.concat([header, payload]));
|
|
28
|
+
return socket.write(Buffer.concat([header, payload]));
|
|
26
29
|
}
|
|
27
30
|
/** Retain fragments until a header or payload is complete; copy each fragmented value once. */
|
|
28
31
|
class MessageBuffer {
|
|
@@ -77,7 +80,7 @@ class MessageBuffer {
|
|
|
77
80
|
* Calls onMessage for each complete message received.
|
|
78
81
|
* Protocol or handler errors are reported to onError so the caller can close the socket.
|
|
79
82
|
*/
|
|
80
|
-
export function createMessageReader(onMessage, onError) {
|
|
83
|
+
export function createMessageReader(onMessage, onError, limit = MAX_FRAME_SIZE_BYTES) {
|
|
81
84
|
const buffer = new MessageBuffer();
|
|
82
85
|
let length;
|
|
83
86
|
return (data) => {
|
|
@@ -85,8 +88,8 @@ export function createMessageReader(onMessage, onError) {
|
|
|
85
88
|
while (buffer.length >= (length ?? 4)) {
|
|
86
89
|
if (length === undefined) {
|
|
87
90
|
length = buffer.read(4).readUInt32BE(0);
|
|
88
|
-
if (length >
|
|
89
|
-
onError(new Error(`Intercom frame too large (${length} bytes; max ${
|
|
91
|
+
if (length > limit) {
|
|
92
|
+
onError(new Error(`Intercom frame too large (${length} bytes; max ${limit})`));
|
|
90
93
|
buffer.clear();
|
|
91
94
|
length = undefined;
|
|
92
95
|
return;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Guarded commands use an exact main, never convenience names or a replacement context. */
|
|
2
|
+
export function guardedDeliveryError(sessions, to, message) {
|
|
3
|
+
const guard = message.guard;
|
|
4
|
+
if (!guard) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const registered = sessions.get(to);
|
|
8
|
+
if (!registered || to !== guard.peerId) {
|
|
9
|
+
return "Guarded target not found";
|
|
10
|
+
}
|
|
11
|
+
const target = registered.info;
|
|
12
|
+
if (target.guardedDelivery !== 1) {
|
|
13
|
+
return "Recipient does not support guarded delivery";
|
|
14
|
+
}
|
|
15
|
+
if (target.parentSessionId !== undefined ||
|
|
16
|
+
target.rootSessionId !== guard.rootSessionId ||
|
|
17
|
+
target.incarnation !== guard.incarnation ||
|
|
18
|
+
target.applicationGuard !== guard.applicationGuard) {
|
|
19
|
+
return "Guarded target changed";
|
|
20
|
+
}
|
|
21
|
+
const mains = [...sessions.values()].filter(({ info }) => info.rootSessionId === guard.rootSessionId && info.parentSessionId === undefined);
|
|
22
|
+
return mains.length === 1 ? null : "Guarded root is ambiguous";
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type net from "node:net";
|
|
2
|
+
import { type UnknownRecord } from "../../shared/unknown.ts";
|
|
3
|
+
import { type LastMessageQuery, type LastMessageResult, type SessionInfo, type ReceiptQuery, type ReceiptResult } from "../types.ts";
|
|
4
|
+
export type NativeReadQuery = LastMessageQuery | ReceiptQuery;
|
|
5
|
+
export type NativeReadResult = LastMessageResult | ReceiptResult;
|
|
6
|
+
export declare function isNativeReadQuery(value: unknown): value is NativeReadQuery;
|
|
7
|
+
export declare function isReceiptResult(value: unknown): value is ReceiptResult;
|
|
8
|
+
export declare function isNativeReadResult(value: unknown): value is NativeReadResult;
|
|
9
|
+
export declare function isLastMessageResult(value: unknown): value is LastMessageResult;
|
|
10
|
+
interface LastPeer {
|
|
11
|
+
readonly socket: net.Socket;
|
|
12
|
+
readonly info: SessionInfo;
|
|
13
|
+
readonly lastMessages: boolean;
|
|
14
|
+
readonly receiptReads: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Correlation belongs to the broker, not the requester or the recipient's model queue. */
|
|
17
|
+
export declare class LastMessageRelay {
|
|
18
|
+
private readonly pending;
|
|
19
|
+
private readonly sessions;
|
|
20
|
+
constructor(sessions: ReadonlyMap<string, LastPeer>);
|
|
21
|
+
request(fromId: string, message: UnknownRecord): void;
|
|
22
|
+
respond(fromId: string, message: UnknownRecord): void;
|
|
23
|
+
disconnected(sessionId: string): void;
|
|
24
|
+
private finish;
|
|
25
|
+
}
|
|
26
|
+
export {};
|