@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.
Files changed (169) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.md +36 -5
  3. package/dist/agent/contracts.js +6 -24
  4. package/dist/agent/host.js +3 -16
  5. package/dist/agent/index.js +207 -173
  6. package/dist/agent/journal-tail.js +115 -156
  7. package/dist/agent/launch.js +20 -8
  8. package/dist/agent/legacy.js +65 -23
  9. package/dist/agent/notifications.js +138 -47
  10. package/dist/agent/observation-state.js +246 -0
  11. package/dist/agent/observation-view.js +258 -0
  12. package/dist/agent/observation.js +346 -0
  13. package/dist/agent/observe-card.js +243 -0
  14. package/dist/agent/observe-chrome.js +85 -0
  15. package/dist/agent/observe-client.js +285 -0
  16. package/dist/agent/observe-layout.js +65 -0
  17. package/dist/agent/observe-reading.js +84 -0
  18. package/dist/agent/observe-screen.js +400 -96
  19. package/dist/agent/observe-transcript.js +382 -0
  20. package/dist/agent/observe.js +250 -43
  21. package/dist/agent/process.js +198 -25
  22. package/dist/agent/receipts.js +92 -20
  23. package/dist/agent/runner.js +125 -20
  24. package/dist/agent/shell.js +2 -1
  25. package/dist/agent/storage.js +29 -12
  26. package/dist/agent/tool-schemas.js +20 -0
  27. package/dist/agent/typebox.js +10 -5
  28. package/dist/agent/usage.js +0 -115
  29. package/dist/agent/view.js +46 -16
  30. package/dist/agent/wake.js +99 -0
  31. package/dist/build-info.js +1 -1
  32. package/dist/intercom-client.d.ts +26 -0
  33. package/dist/intercom-client.js +45 -0
  34. package/dist/pi-intercom/agent-dir.d.ts +1 -0
  35. package/dist/pi-intercom/bridge-session.js +2 -1
  36. package/dist/pi-intercom/broker/broker.js +27 -21
  37. package/dist/pi-intercom/broker/client-last.d.ts +13 -0
  38. package/dist/pi-intercom/broker/client-last.js +17 -5
  39. package/dist/pi-intercom/broker/client.d.ts +60 -0
  40. package/dist/pi-intercom/broker/client.js +47 -16
  41. package/dist/pi-intercom/broker/connection.d.ts +29 -0
  42. package/dist/pi-intercom/broker/framing.d.ts +17 -0
  43. package/dist/pi-intercom/broker/framing.js +10 -7
  44. package/dist/pi-intercom/broker/guard.js +23 -0
  45. package/dist/pi-intercom/broker/last.d.ts +26 -0
  46. package/dist/pi-intercom/broker/last.js +57 -32
  47. package/dist/pi-intercom/broker/paths.d.ts +13 -0
  48. package/dist/pi-intercom/broker/paths.js +35 -12
  49. package/dist/pi-intercom/broker/pid.js +5 -1
  50. package/dist/pi-intercom/broker/server-connection.js +19 -0
  51. package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
  52. package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
  53. package/dist/pi-intercom/broker/targets.d.ts +9 -0
  54. package/dist/pi-intercom/connection.js +10 -2
  55. package/dist/pi-intercom/identity.js +6 -5
  56. package/dist/pi-intercom/inbound-delivery.js +16 -60
  57. package/dist/pi-intercom/inbound-journal.js +83 -6
  58. package/dist/pi-intercom/inbound-record.js +18 -1
  59. package/dist/pi-intercom/inbound.js +12 -38
  60. package/dist/pi-intercom/index.js +3 -0
  61. package/dist/pi-intercom/last-message.js +4 -16
  62. package/dist/pi-intercom/lifecycle.js +88 -3
  63. package/dist/pi-intercom/outbound.js +5 -15
  64. package/dist/pi-intercom/reply-tracker.js +7 -6
  65. package/dist/pi-intercom/reply-wait.js +2 -2
  66. package/dist/pi-intercom/session-targets.d.ts +24 -0
  67. package/dist/pi-intercom/tool-registration.js +4 -1
  68. package/dist/pi-intercom/tool-results.js +5 -7
  69. package/dist/pi-intercom/tool-schemas.js +12 -26
  70. package/dist/pi-intercom/types.d.ts +212 -0
  71. package/dist/pi-intercom/types.js +54 -3
  72. package/dist/pi-intercom/validation.d.ts +12 -0
  73. package/dist/pi-intercom/validation.js +3 -0
  74. package/dist/shared/bounded-json.js +151 -0
  75. package/dist/shared/compact-cards.js +66 -0
  76. package/dist/shared/journal-frames.js +3 -6
  77. package/dist/shared/json-projection.js +267 -129
  78. package/dist/shared/native-journal.js +123 -56
  79. package/dist/shared/post-exit-stdio-guard.js +120 -7
  80. package/dist/shared/tui-action.js +29 -0
  81. package/dist/shared/unknown.d.ts +9 -0
  82. package/docs/code-quality.md +2 -0
  83. package/docs/development.md +40 -1
  84. package/docs/intercom-bridge.md +9 -4
  85. package/docs/intercom.md +226 -6
  86. package/docs/performance.md +87 -11
  87. package/docs/reference.md +157 -20
  88. package/docs/retired-history-maintenance.md +252 -0
  89. package/package.json +28 -15
  90. package/scripts/archive-retired-history.mjs +464 -0
  91. package/scripts/build.mjs +48 -28
  92. package/scripts/measure-size.mjs +1 -1
  93. package/scripts/native-package-smoke.mjs +153 -12
  94. package/scripts/package-smoke.mjs +80 -11
  95. package/scripts/prepare.mjs +2 -2
  96. package/scripts/real-pi-live.mjs +377 -0
  97. package/scripts/real-pi-processes.mjs +6 -1
  98. package/scripts/real-pi-smoke.mjs +6 -282
  99. package/scripts/run-tests.mjs +4 -2
  100. package/scripts/typescript.mjs +35 -0
  101. package/skills/pi-intercom/SKILL.md +2 -1
  102. package/skills/pi-subagents/SKILL.md +23 -5
  103. package/src/agent/contracts.ts +8 -34
  104. package/src/agent/host.ts +19 -20
  105. package/src/agent/index.ts +224 -190
  106. package/src/agent/journal-tail.ts +141 -153
  107. package/src/agent/launch.ts +33 -9
  108. package/src/agent/legacy.ts +85 -31
  109. package/src/agent/notifications.ts +192 -67
  110. package/src/agent/observation-state.ts +277 -0
  111. package/src/agent/observation-view.ts +267 -0
  112. package/src/agent/observation.ts +405 -0
  113. package/src/agent/observe-card.ts +304 -0
  114. package/src/agent/observe-chrome.ts +170 -0
  115. package/src/agent/observe-client.ts +318 -0
  116. package/src/agent/observe-layout.ts +79 -0
  117. package/src/agent/observe-reading.ts +97 -0
  118. package/src/agent/observe-screen.ts +443 -129
  119. package/src/agent/observe-transcript.ts +434 -0
  120. package/src/agent/observe.ts +304 -71
  121. package/src/agent/process.ts +240 -34
  122. package/src/agent/receipts.ts +114 -24
  123. package/src/agent/runner.ts +125 -27
  124. package/src/agent/shell.ts +2 -1
  125. package/src/agent/storage.ts +27 -11
  126. package/src/agent/tool-schemas.ts +33 -0
  127. package/src/agent/typebox.ts +12 -7
  128. package/src/agent/usage.ts +0 -130
  129. package/src/agent/view.ts +70 -20
  130. package/src/agent/wake.ts +107 -0
  131. package/src/intercom-client.ts +108 -0
  132. package/src/pi-intercom/bridge-session.ts +2 -2
  133. package/src/pi-intercom/broker/broker.ts +27 -23
  134. package/src/pi-intercom/broker/client-last.ts +25 -12
  135. package/src/pi-intercom/broker/client.ts +55 -33
  136. package/src/pi-intercom/broker/framing.ts +11 -8
  137. package/src/pi-intercom/broker/guard.ts +33 -0
  138. package/src/pi-intercom/broker/last.ts +86 -33
  139. package/src/pi-intercom/broker/paths.ts +39 -12
  140. package/src/pi-intercom/broker/pid.ts +6 -1
  141. package/src/pi-intercom/broker/server-connection.ts +25 -0
  142. package/src/pi-intercom/connection.ts +21 -10
  143. package/src/pi-intercom/identity.ts +8 -5
  144. package/src/pi-intercom/inbound-delivery.ts +20 -66
  145. package/src/pi-intercom/inbound-journal.ts +103 -7
  146. package/src/pi-intercom/inbound-record.ts +21 -4
  147. package/src/pi-intercom/inbound.ts +18 -49
  148. package/src/pi-intercom/index.ts +3 -0
  149. package/src/pi-intercom/last-message.ts +10 -19
  150. package/src/pi-intercom/lifecycle.ts +117 -14
  151. package/src/pi-intercom/outbound.ts +7 -16
  152. package/src/pi-intercom/reply-tracker.ts +8 -6
  153. package/src/pi-intercom/reply-wait.ts +3 -2
  154. package/src/pi-intercom/runtime-types.ts +0 -1
  155. package/src/pi-intercom/tool-registration.ts +4 -1
  156. package/src/pi-intercom/tool-results.ts +10 -9
  157. package/src/pi-intercom/tool-schemas.ts +12 -44
  158. package/src/pi-intercom/types.ts +115 -4
  159. package/src/pi-intercom/validation.ts +4 -0
  160. package/src/shared/bounded-json.ts +162 -0
  161. package/src/shared/compact-cards.ts +118 -0
  162. package/src/shared/journal-frames.ts +3 -19
  163. package/src/shared/json-projection.ts +267 -147
  164. package/src/shared/native-journal.ts +157 -75
  165. package/src/shared/post-exit-stdio-guard.ts +165 -7
  166. package/src/shared/tui-action.ts +39 -0
  167. package/tsconfig.client.json +8 -0
  168. package/tsconfig.json +1 -0
  169. 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 { assertMainParent, canSeeSession, findSessions } from "./targets.js";
11
- import { acceptBrokerConnection } from "./server-connection.js";
12
- import { compactTopicMessage, isSessionRegistration, normalizeMessage, normalizeSessionInfo, } from "../types.js";
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
- if (!isSessionRegistration(msg.session)) {
110
- throw new Error("Invalid register message");
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 (!target) {
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 { isLastMessageQuery, isLastMessageResult } from "./last.js";
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" || !isLastMessageResult(message.result)) {
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
- request(socket, to, query, timeoutMs) {
27
- if (!isLastMessageQuery(query)) {
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 { isLastMessageQuery, isLastMessageResult } from "./last.js";
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
- lastRequests = new LastMessageRequests();
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 || !isLastMessageQuery(msg.query)) {
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
- 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);
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
- catch (error) {
281
- return Promise.reject(toError(error));
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 (!isLastMessageResult(result)) {
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.requireActiveSocket();
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
- 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,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 {};