@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.
Files changed (180) hide show
  1. package/CHANGELOG.md +63 -2
  2. package/README.md +36 -5
  3. package/dist/agent/child-runtime.js +80 -0
  4. package/dist/agent/contracts.js +15 -24
  5. package/dist/agent/host.js +3 -16
  6. package/dist/agent/index.js +212 -192
  7. package/dist/agent/journal-tail.js +115 -156
  8. package/dist/agent/launch.js +20 -8
  9. package/dist/agent/legacy.js +65 -23
  10. package/dist/agent/notifications.js +138 -47
  11. package/dist/agent/observation-state.js +246 -0
  12. package/dist/agent/observation-view.js +258 -0
  13. package/dist/agent/observation.js +350 -0
  14. package/dist/agent/observe-card.js +243 -0
  15. package/dist/agent/observe-chrome.js +85 -0
  16. package/dist/agent/observe-client.js +285 -0
  17. package/dist/agent/observe-layout.js +65 -0
  18. package/dist/agent/observe-reading.js +84 -0
  19. package/dist/agent/observe-screen.js +400 -96
  20. package/dist/agent/observe-transcript.js +382 -0
  21. package/dist/agent/observe.js +250 -43
  22. package/dist/agent/parking.js +253 -0
  23. package/dist/agent/process.js +228 -28
  24. package/dist/agent/receipts.js +92 -20
  25. package/dist/agent/runner.js +229 -126
  26. package/dist/agent/shell.js +2 -1
  27. package/dist/agent/storage.js +47 -12
  28. package/dist/agent/tool-schemas.js +20 -0
  29. package/dist/agent/typebox.js +10 -5
  30. package/dist/agent/usage.js +0 -115
  31. package/dist/agent/view.js +46 -16
  32. package/dist/agent/wake.js +99 -0
  33. package/dist/build-info.js +1 -1
  34. package/dist/intercom-client.d.ts +26 -0
  35. package/dist/intercom-client.js +45 -0
  36. package/dist/pi-intercom/agent-dir.d.ts +1 -0
  37. package/dist/pi-intercom/bridge-session.js +2 -1
  38. package/dist/pi-intercom/broker/broker.js +62 -43
  39. package/dist/pi-intercom/broker/client-last.d.ts +24 -0
  40. package/dist/pi-intercom/broker/client-last.js +42 -5
  41. package/dist/pi-intercom/broker/client-park.d.ts +9 -0
  42. package/dist/pi-intercom/broker/client-park.js +21 -0
  43. package/dist/pi-intercom/broker/client.d.ts +59 -0
  44. package/dist/pi-intercom/broker/client.js +55 -29
  45. package/dist/pi-intercom/broker/connection.d.ts +29 -0
  46. package/dist/pi-intercom/broker/framing.d.ts +17 -0
  47. package/dist/pi-intercom/broker/framing.js +10 -7
  48. package/dist/pi-intercom/broker/guard.js +23 -0
  49. package/dist/pi-intercom/broker/last.d.ts +30 -0
  50. package/dist/pi-intercom/broker/last.js +87 -40
  51. package/dist/pi-intercom/broker/parked.js +107 -0
  52. package/dist/pi-intercom/broker/paths.d.ts +13 -0
  53. package/dist/pi-intercom/broker/paths.js +35 -12
  54. package/dist/pi-intercom/broker/pid.js +5 -1
  55. package/dist/pi-intercom/broker/server-connection.js +20 -1
  56. package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
  57. package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
  58. package/dist/pi-intercom/broker/targets.d.ts +9 -0
  59. package/dist/pi-intercom/connection.js +71 -20
  60. package/dist/pi-intercom/identity.js +6 -5
  61. package/dist/pi-intercom/inbound-delivery.js +16 -60
  62. package/dist/pi-intercom/inbound-journal.js +83 -6
  63. package/dist/pi-intercom/inbound-record.js +18 -1
  64. package/dist/pi-intercom/inbound.js +22 -37
  65. package/dist/pi-intercom/index.js +41 -0
  66. package/dist/pi-intercom/last-message.js +4 -16
  67. package/dist/pi-intercom/lifecycle.js +88 -3
  68. package/dist/pi-intercom/outbound.js +5 -15
  69. package/dist/pi-intercom/reply-tracker.js +7 -6
  70. package/dist/pi-intercom/reply-wait.js +2 -2
  71. package/dist/pi-intercom/session-targets.d.ts +24 -0
  72. package/dist/pi-intercom/tool-registration.js +4 -1
  73. package/dist/pi-intercom/tool-results.js +5 -7
  74. package/dist/pi-intercom/tool-schemas.js +12 -26
  75. package/dist/pi-intercom/types.d.ts +212 -0
  76. package/dist/pi-intercom/types.js +54 -3
  77. package/dist/pi-intercom/validation.d.ts +12 -0
  78. package/dist/pi-intercom/validation.js +3 -0
  79. package/dist/shared/bounded-json.js +151 -0
  80. package/dist/shared/compact-cards.js +66 -0
  81. package/dist/shared/journal-frames.js +3 -6
  82. package/dist/shared/json-projection.js +267 -129
  83. package/dist/shared/native-journal.js +123 -56
  84. package/dist/shared/parking.js +12 -0
  85. package/dist/shared/post-exit-stdio-guard.js +120 -7
  86. package/dist/shared/tui-action.js +29 -0
  87. package/dist/shared/unknown.d.ts +9 -0
  88. package/docs/code-quality.md +5 -3
  89. package/docs/development.md +42 -3
  90. package/docs/intercom-bridge.md +9 -4
  91. package/docs/intercom.md +228 -7
  92. package/docs/performance.md +118 -14
  93. package/docs/reference.md +168 -20
  94. package/docs/retired-history-maintenance.md +252 -0
  95. package/package.json +28 -15
  96. package/scripts/archive-retired-history.mjs +464 -0
  97. package/scripts/build.mjs +48 -28
  98. package/scripts/measure-size.mjs +1 -1
  99. package/scripts/native-package-smoke.mjs +153 -12
  100. package/scripts/package-smoke.mjs +80 -11
  101. package/scripts/prepare.mjs +2 -2
  102. package/scripts/real-pi-live.mjs +377 -0
  103. package/scripts/real-pi-processes.mjs +6 -1
  104. package/scripts/real-pi-smoke.mjs +6 -282
  105. package/scripts/run-tests.mjs +4 -2
  106. package/scripts/typescript.mjs +35 -0
  107. package/skills/pi-intercom/SKILL.md +2 -1
  108. package/skills/pi-subagents/SKILL.md +23 -5
  109. package/src/agent/child-runtime.ts +105 -0
  110. package/src/agent/contracts.ts +18 -34
  111. package/src/agent/host.ts +39 -20
  112. package/src/agent/index.ts +234 -207
  113. package/src/agent/journal-tail.ts +141 -153
  114. package/src/agent/launch.ts +33 -9
  115. package/src/agent/legacy.ts +85 -31
  116. package/src/agent/notifications.ts +192 -67
  117. package/src/agent/observation-state.ts +277 -0
  118. package/src/agent/observation-view.ts +267 -0
  119. package/src/agent/observation.ts +409 -0
  120. package/src/agent/observe-card.ts +304 -0
  121. package/src/agent/observe-chrome.ts +170 -0
  122. package/src/agent/observe-client.ts +318 -0
  123. package/src/agent/observe-layout.ts +79 -0
  124. package/src/agent/observe-reading.ts +97 -0
  125. package/src/agent/observe-screen.ts +443 -129
  126. package/src/agent/observe-transcript.ts +434 -0
  127. package/src/agent/observe.ts +304 -71
  128. package/src/agent/parking.ts +341 -0
  129. package/src/agent/process.ts +280 -37
  130. package/src/agent/receipts.ts +114 -24
  131. package/src/agent/runner.ts +227 -149
  132. package/src/agent/shell.ts +2 -1
  133. package/src/agent/storage.ts +44 -11
  134. package/src/agent/tool-schemas.ts +33 -0
  135. package/src/agent/typebox.ts +12 -7
  136. package/src/agent/usage.ts +0 -130
  137. package/src/agent/view.ts +70 -20
  138. package/src/agent/wake.ts +107 -0
  139. package/src/intercom-client.ts +108 -0
  140. package/src/pi-intercom/bridge-session.ts +2 -2
  141. package/src/pi-intercom/broker/broker.ts +64 -53
  142. package/src/pi-intercom/broker/client-last.ts +65 -12
  143. package/src/pi-intercom/broker/client-park.ts +23 -0
  144. package/src/pi-intercom/broker/client.ts +63 -45
  145. package/src/pi-intercom/broker/framing.ts +11 -8
  146. package/src/pi-intercom/broker/guard.ts +33 -0
  147. package/src/pi-intercom/broker/last.ts +131 -43
  148. package/src/pi-intercom/broker/parked.ts +130 -0
  149. package/src/pi-intercom/broker/paths.ts +39 -12
  150. package/src/pi-intercom/broker/pid.ts +6 -1
  151. package/src/pi-intercom/broker/server-connection.ts +27 -2
  152. package/src/pi-intercom/connection.ts +103 -39
  153. package/src/pi-intercom/identity.ts +8 -5
  154. package/src/pi-intercom/inbound-delivery.ts +20 -66
  155. package/src/pi-intercom/inbound-journal.ts +103 -7
  156. package/src/pi-intercom/inbound-record.ts +21 -4
  157. package/src/pi-intercom/inbound.ts +32 -49
  158. package/src/pi-intercom/index.ts +44 -0
  159. package/src/pi-intercom/last-message.ts +11 -20
  160. package/src/pi-intercom/lifecycle.ts +117 -14
  161. package/src/pi-intercom/outbound.ts +7 -16
  162. package/src/pi-intercom/reply-tracker.ts +8 -6
  163. package/src/pi-intercom/reply-wait.ts +3 -2
  164. package/src/pi-intercom/runtime-types.ts +0 -1
  165. package/src/pi-intercom/tool-registration.ts +4 -1
  166. package/src/pi-intercom/tool-results.ts +10 -9
  167. package/src/pi-intercom/tool-schemas.ts +12 -44
  168. package/src/pi-intercom/types.ts +115 -4
  169. package/src/pi-intercom/validation.ts +4 -0
  170. package/src/shared/bounded-json.ts +162 -0
  171. package/src/shared/compact-cards.ts +118 -0
  172. package/src/shared/journal-frames.ts +3 -19
  173. package/src/shared/json-projection.ts +267 -147
  174. package/src/shared/native-journal.ts +157 -75
  175. package/src/shared/parking.ts +22 -0
  176. package/src/shared/post-exit-stdio-guard.ts +165 -7
  177. package/src/shared/tui-action.ts +39 -0
  178. package/tsconfig.client.json +8 -0
  179. package/tsconfig.json +1 -0
  180. 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 { isLastMessageQuery, isLastMessageResult } from "./last.js";
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" || !isLastMessageResult(message.result)) {
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
- request(socket, to, query, timeoutMs) {
27
- if (!isLastMessageQuery(query)) {
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 { isLastMessageQuery, isLastMessageResult } from "./last.js";
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
- lastMessagesSupported = false;
34
- lastRequests = new LastMessageRequests();
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.lastMessagesSupported = false;
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("Invalid session_left message");
192
+ throw new Error(`Invalid ${type} message`);
179
193
  }
180
- this.emit("session_left", brokerMessage.sessionId);
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.lastMessagesSupported = msg.lastMessages === true;
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 from = normalizeSessionInfo(msg.from);
201
- if (typeof msg.requestId !== "string" || !from || !isLastMessageQuery(msg.query)) {
202
- throw new Error("Invalid last-message request");
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
- try {
274
- const socket = this.requireActiveSocket();
275
- if (!this.lastMessagesSupported) {
276
- throw new Error("This broker does not support last-message reads; reload after it exits.");
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 (!isLastMessageResult(result)) {
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.requireActiveSocket();
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
- const json = JSON.stringify(msg);
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 > MAX_FRAME_SIZE_BYTES) {
20
- throw new Error(`Intercom message too large (${size} bytes; max ${MAX_FRAME_SIZE_BYTES})`);
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 > MAX_FRAME_SIZE_BYTES) {
89
- onError(new Error(`Intercom frame too large (${length} bytes; max ${MAX_FRAME_SIZE_BYTES})`));
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 {};