@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
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type net from "node:net";
|
|
2
|
+
import { type UnknownRecord } from "../../shared/unknown.ts";
|
|
3
|
+
import { type NativeReadQuery } from "./last.ts";
|
|
4
|
+
import { type LastMessageQuery, type LastMessageResult, type ReceiptQuery, type ReceiptResult, type SessionInfo } from "../types.ts";
|
|
5
|
+
/** A peer's read of this session or, for a main, of its exited `parked` child. */
|
|
6
|
+
export declare function lastRequest(msg: UnknownRecord): {
|
|
7
|
+
readonly requestId: string;
|
|
8
|
+
readonly from: SessionInfo;
|
|
9
|
+
readonly query: NativeReadQuery;
|
|
10
|
+
readonly parked?: SessionInfo;
|
|
11
|
+
};
|
|
12
|
+
export declare class LastMessageRequests {
|
|
13
|
+
/** Negotiated at registration; a failed connection supports neither. */
|
|
14
|
+
lastMessages: boolean;
|
|
15
|
+
receiptReads: boolean;
|
|
16
|
+
private readonly timeoutMs;
|
|
17
|
+
constructor(timeoutMs: number);
|
|
18
|
+
private readonly pending;
|
|
19
|
+
fail(error: Readonly<Error>): void;
|
|
20
|
+
receive(message: UnknownRecord): void;
|
|
21
|
+
last(socket: net.Socket, to: string, query: LastMessageQuery): Promise<LastMessageResult>;
|
|
22
|
+
receipt(socket: net.Socket, to: string, query: ReceiptQuery): Promise<ReceiptResult>;
|
|
23
|
+
private request;
|
|
24
|
+
}
|
|
@@ -1,10 +1,33 @@
|
|
|
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
|
+
import { normalizeSessionInfo, } from "../types.js";
|
|
4
5
|
import { writeMessage } from "./framing.js";
|
|
6
|
+
/** A peer's read of this session or, for a main, of its exited `parked` child. */
|
|
7
|
+
export function lastRequest(msg) {
|
|
8
|
+
const from = normalizeSessionInfo(msg.from);
|
|
9
|
+
if (typeof msg.requestId !== "string" || !from || !isNativeReadQuery(msg.query)) {
|
|
10
|
+
throw new Error("Invalid last-message request");
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
requestId: msg.requestId,
|
|
14
|
+
from,
|
|
15
|
+
query: msg.query,
|
|
16
|
+
parked: normalizeSessionInfo(msg.parked) ?? undefined,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
5
19
|
export class LastMessageRequests {
|
|
20
|
+
/** Negotiated at registration; a failed connection supports neither. */
|
|
21
|
+
lastMessages = false;
|
|
22
|
+
receiptReads = false;
|
|
23
|
+
timeoutMs;
|
|
24
|
+
constructor(timeoutMs) {
|
|
25
|
+
this.timeoutMs = timeoutMs;
|
|
26
|
+
}
|
|
6
27
|
pending = new Map();
|
|
7
28
|
fail(error) {
|
|
29
|
+
this.lastMessages = false;
|
|
30
|
+
this.receiptReads = false;
|
|
8
31
|
for (const pending of this.pending.values()) {
|
|
9
32
|
clearTimeout(pending.timer);
|
|
10
33
|
pending.reject(error);
|
|
@@ -12,7 +35,7 @@ export class LastMessageRequests {
|
|
|
12
35
|
this.pending.clear();
|
|
13
36
|
}
|
|
14
37
|
receive(message) {
|
|
15
|
-
if (typeof message.requestId !== "string" || !
|
|
38
|
+
if (typeof message.requestId !== "string" || !isNativeReadResult(message.result)) {
|
|
16
39
|
throw new Error("Invalid last-message response");
|
|
17
40
|
}
|
|
18
41
|
const pending = this.pending.get(message.requestId);
|
|
@@ -23,16 +46,30 @@ export class LastMessageRequests {
|
|
|
23
46
|
this.pending.delete(message.requestId);
|
|
24
47
|
pending.resolve(message.result);
|
|
25
48
|
}
|
|
26
|
-
|
|
27
|
-
|
|
49
|
+
async last(socket, to, query) {
|
|
50
|
+
const result = await this.request(socket, to, query);
|
|
51
|
+
return isLastMessageResult(result) ? result : { error: "Invalid last-message response" };
|
|
52
|
+
}
|
|
53
|
+
async receipt(socket, to, query) {
|
|
54
|
+
const result = await this.request(socket, to, query);
|
|
55
|
+
return isReceiptResult(result) ? result : { error: "Invalid receipt response" };
|
|
56
|
+
}
|
|
57
|
+
request(socket, to, query) {
|
|
58
|
+
if (!this.lastMessages) {
|
|
59
|
+
return Promise.reject(new Error("This broker does not support last-message reads; reload after it exits."));
|
|
60
|
+
}
|
|
61
|
+
if (!isNativeReadQuery(query)) {
|
|
28
62
|
return Promise.reject(new Error("Invalid last-message query"));
|
|
29
63
|
}
|
|
64
|
+
if ("messageId" in query && !this.receiptReads) {
|
|
65
|
+
return Promise.reject(new Error("publication_unavailable"));
|
|
66
|
+
}
|
|
30
67
|
return new Promise((resolve, reject) => {
|
|
31
68
|
const requestId = randomUUID();
|
|
32
69
|
const timer = setTimeout(() => {
|
|
33
70
|
this.pending.delete(requestId);
|
|
34
71
|
reject(new Error("Last-message read timeout"));
|
|
35
|
-
}, timeoutMs);
|
|
72
|
+
}, this.timeoutMs);
|
|
36
73
|
timer.unref();
|
|
37
74
|
this.pending.set(requestId, { resolve, reject, timer });
|
|
38
75
|
try {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type net from "node:net";
|
|
2
|
+
/** Parking negotiation and its single outstanding broker acknowledgement. */
|
|
3
|
+
export declare class ParkRequests {
|
|
4
|
+
supported: boolean;
|
|
5
|
+
private acknowledged?;
|
|
6
|
+
settle(parked: boolean): void;
|
|
7
|
+
/** True once the broker retains later input; false when it cannot or the socket closed. */
|
|
8
|
+
request(socket: () => net.Socket, on: boolean): Promise<boolean>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { writeMessage } from "./framing.js";
|
|
2
|
+
/** Parking negotiation and its single outstanding broker acknowledgement. */
|
|
3
|
+
export class ParkRequests {
|
|
4
|
+
supported = false;
|
|
5
|
+
acknowledged;
|
|
6
|
+
settle(parked) {
|
|
7
|
+
this.acknowledged?.(parked);
|
|
8
|
+
}
|
|
9
|
+
/** True once the broker retains later input; false when it cannot or the socket closed. */
|
|
10
|
+
request(socket, on) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
this.acknowledged = on && this.supported ? resolve : undefined;
|
|
13
|
+
if (this.supported) {
|
|
14
|
+
writeMessage(socket(), { type: on ? "park" : "unpark" });
|
|
15
|
+
}
|
|
16
|
+
if (this.acknowledged === undefined) {
|
|
17
|
+
resolve(false);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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 readonly parking;
|
|
15
|
+
private guardedDeliverySupported;
|
|
16
|
+
private readonly lastRequests;
|
|
17
|
+
private readonly pendingSends;
|
|
18
|
+
private readonly pendingLists;
|
|
19
|
+
private connecting;
|
|
20
|
+
private disconnecting;
|
|
21
|
+
private readonly sendTimeoutMs;
|
|
22
|
+
private readonly listTimeoutMs;
|
|
23
|
+
constructor(options?: IntercomClientOptions);
|
|
24
|
+
private failPending;
|
|
25
|
+
get sessionId(): string | null;
|
|
26
|
+
get supportsTopics(): boolean;
|
|
27
|
+
get supportsGuardedDelivery(): boolean;
|
|
28
|
+
isConnected(): boolean;
|
|
29
|
+
private requireActiveSocket;
|
|
30
|
+
connect(session: Omit<SessionInfo, "id">, requestedId?: string): Promise<void>;
|
|
31
|
+
private restoreTopics;
|
|
32
|
+
private handleBrokerMessage;
|
|
33
|
+
private receiveRegistration;
|
|
34
|
+
private receiveLastRequest;
|
|
35
|
+
private receiveMessage;
|
|
36
|
+
private receiveDelivery;
|
|
37
|
+
private receiveSessions;
|
|
38
|
+
park(on?: boolean): Promise<boolean>;
|
|
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
|
+
respondLast(requestId: string, result: unknown): void;
|
|
44
|
+
updateTopics(change: TopicChange, onAccepted?: (snapshot: SessionSnapshot) => void): Promise<SessionSnapshot>;
|
|
45
|
+
private requestSessions;
|
|
46
|
+
private requireSendSocket;
|
|
47
|
+
send(to: string, options: SendOptions): Promise<SendResult>;
|
|
48
|
+
updatePresence(updates: {
|
|
49
|
+
readonly applicationGuard?: string | null;
|
|
50
|
+
readonly name?: string;
|
|
51
|
+
readonly status?: string;
|
|
52
|
+
readonly model?: string;
|
|
53
|
+
readonly pendingAsks?: number;
|
|
54
|
+
readonly acceptsAsks?: boolean;
|
|
55
|
+
readonly lastIntercomActivity?: number;
|
|
56
|
+
readonly subscriptions?: SessionInfo["subscriptions"];
|
|
57
|
+
readonly topics?: readonly TopicUpdate[];
|
|
58
|
+
}): void;
|
|
59
|
+
}
|
|
@@ -3,8 +3,9 @@ import { randomUUID } from "crypto";
|
|
|
3
3
|
import { writeMessage } from "./framing.js";
|
|
4
4
|
import { BrokerConnection, connectBrokerSocket } from "./connection.js";
|
|
5
5
|
import { SnapshotReader } from "./snapshot-reader.js";
|
|
6
|
-
import { LastMessageRequests } from "./client-last.js";
|
|
7
|
-
import {
|
|
6
|
+
import { LastMessageRequests, lastRequest } from "./client-last.js";
|
|
7
|
+
import { ParkRequests } from "./client-park.js";
|
|
8
|
+
import { isNativeReadResult } from "./last.js";
|
|
8
9
|
import { errorMessage, isRecord, isUnknownArray, } from "../../shared/unknown.js";
|
|
9
10
|
import { compactTopicMessage, normalizeMessage, normalizeSessionInfo, } from "../types.js";
|
|
10
11
|
/** Default delivery-ack timeout for `send` (broker acknowledges quickly). */
|
|
@@ -30,8 +31,9 @@ export class IntercomClient extends EventEmitter {
|
|
|
30
31
|
connection = null;
|
|
31
32
|
_sessionId = null;
|
|
32
33
|
_topicsSupported = false;
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
parking = new ParkRequests();
|
|
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 &&
|
|
@@ -98,7 +104,8 @@ export class IntercomClient extends EventEmitter {
|
|
|
98
104
|
this.connection = null;
|
|
99
105
|
this._sessionId = null;
|
|
100
106
|
this._topicsSupported = false;
|
|
101
|
-
this.
|
|
107
|
+
this.parking.settle(false);
|
|
108
|
+
this.guardedDeliverySupported = false;
|
|
102
109
|
this.disconnecting = false;
|
|
103
110
|
this.failPending(error);
|
|
104
111
|
if (established && !expected) {
|
|
@@ -115,6 +122,9 @@ export class IntercomClient extends EventEmitter {
|
|
|
115
122
|
requestedId,
|
|
116
123
|
topicFrames: true,
|
|
117
124
|
lastMessages: true,
|
|
125
|
+
parking: true,
|
|
126
|
+
receiptReads: 1,
|
|
127
|
+
guardedDelivery: 1,
|
|
118
128
|
});
|
|
119
129
|
}
|
|
120
130
|
catch (error) {
|
|
@@ -173,11 +183,15 @@ export class IntercomClient extends EventEmitter {
|
|
|
173
183
|
case "delivery_failed":
|
|
174
184
|
this.receiveDelivery(brokerMessage);
|
|
175
185
|
break;
|
|
186
|
+
case "parked":
|
|
187
|
+
this.parking.settle(true);
|
|
188
|
+
break;
|
|
189
|
+
case "wake":
|
|
176
190
|
case "session_left":
|
|
177
191
|
if (typeof brokerMessage.sessionId !== "string") {
|
|
178
|
-
throw new Error(
|
|
192
|
+
throw new Error(`Invalid ${type} message`);
|
|
179
193
|
}
|
|
180
|
-
this.emit(
|
|
194
|
+
this.emit(type, brokerMessage.sessionId);
|
|
181
195
|
break;
|
|
182
196
|
default:
|
|
183
197
|
throw new Error(`Unknown broker message type: ${type}`);
|
|
@@ -192,17 +206,17 @@ export class IntercomClient extends EventEmitter {
|
|
|
192
206
|
}
|
|
193
207
|
this._sessionId = msg.sessionId;
|
|
194
208
|
this._topicsSupported = msg.topicsSupported === true && msg.topicFrames === true;
|
|
195
|
-
this.
|
|
209
|
+
this.lastRequests.lastMessages = msg.lastMessages === true;
|
|
210
|
+
this.parking.supported = msg.parking === true;
|
|
211
|
+
this.lastRequests.receiptReads = msg.receiptReads === 1;
|
|
212
|
+
this.guardedDeliverySupported = msg.guardedDelivery === 1;
|
|
196
213
|
this.connection?.confirmRegistration();
|
|
197
214
|
this.emit("_registered", { type: "registered", sessionId: msg.sessionId });
|
|
198
215
|
}
|
|
199
216
|
receiveLastRequest(msg) {
|
|
200
|
-
const
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
if (!this.emit("last_request", { requestId: msg.requestId, from, query: msg.query })) {
|
|
205
|
-
this.respondLast(msg.requestId, { error: "Recipient has no last-message reader." });
|
|
217
|
+
const request = lastRequest(msg);
|
|
218
|
+
if (!this.emit("last_request", request)) {
|
|
219
|
+
this.respondLast(request.requestId, { error: "Recipient has no last-message reader." });
|
|
206
220
|
}
|
|
207
221
|
}
|
|
208
222
|
receiveMessage(msg) {
|
|
@@ -257,6 +271,9 @@ export class IntercomClient extends EventEmitter {
|
|
|
257
271
|
pending.resolve(pending.reader.finish());
|
|
258
272
|
}
|
|
259
273
|
}
|
|
274
|
+
park(on = true) {
|
|
275
|
+
return this.parking.request(() => this.requireActiveSocket(), on);
|
|
276
|
+
}
|
|
260
277
|
async disconnect() {
|
|
261
278
|
const connection = this.connection;
|
|
262
279
|
if (!connection) {
|
|
@@ -269,20 +286,14 @@ export class IntercomClient extends EventEmitter {
|
|
|
269
286
|
listSessions() {
|
|
270
287
|
return this.requestSessions().then((snapshot) => [...snapshot.sessions]);
|
|
271
288
|
}
|
|
272
|
-
last(to, query = {}) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
return this.lastRequests.request(socket, to, query, this.listTimeoutMs);
|
|
279
|
-
}
|
|
280
|
-
catch (error) {
|
|
281
|
-
return Promise.reject(toError(error));
|
|
282
|
-
}
|
|
289
|
+
async last(to, query = {}) {
|
|
290
|
+
return await this.lastRequests.last(this.requireActiveSocket(), to, query);
|
|
291
|
+
}
|
|
292
|
+
async receipt(to, query) {
|
|
293
|
+
return await this.lastRequests.receipt(this.requireActiveSocket(), to, query);
|
|
283
294
|
}
|
|
284
295
|
respondLast(requestId, result) {
|
|
285
|
-
if (!
|
|
296
|
+
if (!isNativeReadResult(result)) {
|
|
286
297
|
throw new Error("Invalid last-message result");
|
|
287
298
|
}
|
|
288
299
|
writeMessage(this.requireActiveSocket(), { type: "last_response", requestId, result });
|
|
@@ -321,7 +332,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
321
332
|
const timeout = setTimeout(() => {
|
|
322
333
|
if (this.pendingLists.has(requestId)) {
|
|
323
334
|
this.pendingLists.delete(requestId);
|
|
324
|
-
wrappedReject(new Error("List sessions timeout"));
|
|
335
|
+
wrappedReject(Object.assign(new Error("List sessions timeout"), { code: "list_timeout" }));
|
|
325
336
|
}
|
|
326
337
|
}, this.listTimeoutMs);
|
|
327
338
|
timeout.unref();
|
|
@@ -345,20 +356,32 @@ export class IntercomClient extends EventEmitter {
|
|
|
345
356
|
}
|
|
346
357
|
});
|
|
347
358
|
}
|
|
359
|
+
requireSendSocket(options) {
|
|
360
|
+
const socket = this.requireActiveSocket();
|
|
361
|
+
if (options.guard && !this.guardedDeliverySupported) {
|
|
362
|
+
throw new Error("This broker does not support guarded delivery; no message sent.");
|
|
363
|
+
}
|
|
364
|
+
return socket;
|
|
365
|
+
}
|
|
348
366
|
send(to, options) {
|
|
349
367
|
let socket;
|
|
350
368
|
try {
|
|
351
|
-
socket = this.
|
|
369
|
+
socket = this.requireSendSocket(options);
|
|
352
370
|
}
|
|
353
371
|
catch (error) {
|
|
354
372
|
return Promise.reject(toError(error));
|
|
355
373
|
}
|
|
356
|
-
const messageId = options.messageId ?? randomUUID();
|
|
374
|
+
const messageId = options.messageId ?? (options.guard ? "" : randomUUID());
|
|
375
|
+
if (this.pendingSends.has(messageId)) {
|
|
376
|
+
return Promise.reject(new Error("Message ID already has a pending send"));
|
|
377
|
+
}
|
|
357
378
|
const message = {
|
|
358
379
|
id: messageId,
|
|
359
380
|
timestamp: Date.now(),
|
|
360
381
|
replyTo: options.replyTo,
|
|
361
382
|
expectsReply: options.expectsReply,
|
|
383
|
+
guard: options.guard,
|
|
384
|
+
replyHint: options.replyHint,
|
|
362
385
|
...(options.human ? { human: options.human } : {}),
|
|
363
386
|
...(options.topic ? { topic: options.topic } : {}),
|
|
364
387
|
delivery: messageDelivery(options),
|
|
@@ -370,6 +393,9 @@ export class IntercomClient extends EventEmitter {
|
|
|
370
393
|
attachments: options.attachments,
|
|
371
394
|
},
|
|
372
395
|
};
|
|
396
|
+
if ((options.guard || options.replyHint) && normalizeMessage(message) === null) {
|
|
397
|
+
return Promise.reject(new Error("Invalid message format"));
|
|
398
|
+
}
|
|
373
399
|
return new Promise((resolve, reject) => {
|
|
374
400
|
const wrappedResolve = (result) => {
|
|
375
401
|
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,30 @@
|
|
|
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
|
+
readonly parked?: {
|
|
16
|
+
readonly closed: boolean;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Correlation belongs to the broker, not the requester or the recipient's model queue. */
|
|
20
|
+
export declare class LastMessageRelay {
|
|
21
|
+
private readonly pending;
|
|
22
|
+
private readonly sessions;
|
|
23
|
+
constructor(sessions: ReadonlyMap<string, LastPeer>);
|
|
24
|
+
/** `reader` names another peer that answers for an exited target from its captured history. */
|
|
25
|
+
request(fromId: string, input: UnknownRecord, reader: (id: string) => LastPeer | undefined): void;
|
|
26
|
+
respond(fromId: string, message: UnknownRecord): void;
|
|
27
|
+
disconnected(sessionId: string): void;
|
|
28
|
+
private finish;
|
|
29
|
+
}
|
|
30
|
+
export {};
|