@fitchmultz/pi-subagents 0.52.4 → 0.52.5

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 (64) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +5 -4
  3. package/dist/agent/completion-watches.js +61 -0
  4. package/dist/agent/contracts.js +9 -0
  5. package/dist/agent/host.js +5 -0
  6. package/dist/agent/index.js +20 -51
  7. package/dist/agent/legacy.js +41 -11
  8. package/dist/agent/notifications.js +2 -2
  9. package/dist/agent/parking.js +121 -13
  10. package/dist/agent/process.js +107 -63
  11. package/dist/agent/receipts.js +21 -4
  12. package/dist/agent/runner.js +19 -5
  13. package/dist/agent/shutdown.js +43 -0
  14. package/dist/agent/view.js +5 -1
  15. package/dist/build-info.js +1 -1
  16. package/dist/pi-intercom/broker/broker.js +7 -6
  17. package/dist/pi-intercom/broker/client-wake.d.ts +17 -0
  18. package/dist/pi-intercom/broker/client-wake.js +54 -0
  19. package/dist/pi-intercom/broker/client.d.ts +2 -0
  20. package/dist/pi-intercom/broker/client.js +23 -16
  21. package/dist/pi-intercom/broker/parked.js +122 -5
  22. package/dist/pi-intercom/broker/spawn.js +10 -6
  23. package/dist/pi-intercom/config.js +3 -2
  24. package/dist/pi-intercom/connection.js +72 -11
  25. package/dist/pi-intercom/inbound-delivery.js +10 -5
  26. package/dist/pi-intercom/index.js +41 -6
  27. package/dist/pi-intercom/outbound.js +24 -7
  28. package/dist/pi-intercom/reply-wait.js +6 -1
  29. package/dist/pi-intercom/tool-results.js +2 -0
  30. package/dist/pi-intercom/types.d.ts +6 -0
  31. package/dist/shared/background-reporting.js +72 -0
  32. package/dist/shared/parking.js +6 -0
  33. package/docs/development.md +2 -0
  34. package/docs/intercom.md +14 -0
  35. package/docs/performance.md +2 -2
  36. package/docs/reference.md +13 -5
  37. package/package.json +1 -1
  38. package/src/agent/completion-watches.ts +73 -0
  39. package/src/agent/contracts.ts +11 -0
  40. package/src/agent/host.ts +6 -0
  41. package/src/agent/index.ts +24 -54
  42. package/src/agent/legacy.ts +44 -12
  43. package/src/agent/notifications.ts +2 -3
  44. package/src/agent/parking.ts +148 -12
  45. package/src/agent/process.ts +130 -66
  46. package/src/agent/receipts.ts +25 -4
  47. package/src/agent/runner.ts +25 -5
  48. package/src/agent/shutdown.ts +50 -0
  49. package/src/agent/view.ts +9 -0
  50. package/src/pi-intercom/broker/broker.ts +8 -7
  51. package/src/pi-intercom/broker/client-wake.ts +61 -0
  52. package/src/pi-intercom/broker/client.ts +23 -17
  53. package/src/pi-intercom/broker/parked.ts +152 -10
  54. package/src/pi-intercom/broker/spawn.ts +10 -9
  55. package/src/pi-intercom/config.ts +5 -2
  56. package/src/pi-intercom/connection.ts +88 -12
  57. package/src/pi-intercom/inbound-delivery.ts +21 -6
  58. package/src/pi-intercom/index.ts +64 -21
  59. package/src/pi-intercom/outbound.ts +30 -6
  60. package/src/pi-intercom/reply-wait.ts +6 -1
  61. package/src/pi-intercom/tool-results.ts +1 -0
  62. package/src/pi-intercom/types.ts +4 -0
  63. package/src/shared/background-reporting.ts +78 -0
  64. package/src/shared/parking.ts +16 -0
@@ -7,8 +7,8 @@ import { setTimeout as delay } from "node:timers/promises";
7
7
  import { isChildProcessReplaced, observeChildProcessReplaced, readChildProcessIdentity, readProcessClock, trySignalChildTree, } from "../shared/post-exit-stdio-guard.js";
8
8
  import { agentObservationSocketPath } from "../pi-intercom/broker/paths.js";
9
9
  import { hasErrorCode } from "../shared/unknown.js";
10
- import { attachmentSchema, parkedSchema, processSchema, } from "./contracts.js";
11
- import { conversation, enqueue, launchAt, list, read, write } from "./storage.js";
10
+ import { attachmentSchema, parkedSchema, processSchema, wakeFailureSchema, } from "./contracts.js";
11
+ import { conversation, enqueue, read, write } from "./storage.js";
12
12
  import { errorText } from "./host.js";
13
13
  /** Owner records follow their process's birth; stat after read only moves the bound later. */
14
14
  export function published(file, record) {
@@ -119,33 +119,64 @@ function childEnvironment(launch) {
119
119
  PI_INTERCOM_PARENT_SESSION_ID: launch.owner,
120
120
  });
121
121
  }
122
- function spawnActor(base, launch, token) {
122
+ function saveSpawnFailure(dir, error) {
123
+ try {
124
+ write(path.join(dir, "spawn-error.json"), { error: error.message });
125
+ return error;
126
+ }
127
+ catch (failure) {
128
+ return Object.assign(new Error(`${error.message}; could not save the spawn diagnostic: ${errorText(failure)}`, {
129
+ cause: error,
130
+ }), "code" in error ? { code: error.code } : {});
131
+ }
132
+ }
133
+ async function spawnActor(base, launch, token, claim) {
123
134
  const dir = conversation(base, launch);
124
- const log = fs.openSync(path.join(dir, "runner.log"), "a", 0o600);
125
- const runner = fileURLToPath(new URL(import.meta.url.endsWith(".ts") ? "./runner.ts" : "./runner.js", import.meta.url));
126
135
  let child;
136
+ let spawnError;
127
137
  try {
128
- child = spawn(process.execPath, [runner, base, launch.rootId, token, launch.id], {
129
- cwd: launch.cwd,
130
- env: childEnvironment(launch),
131
- detached: true,
132
- stdio: ["ignore", log, log],
133
- });
138
+ const log = fs.openSync(path.join(dir, "runner.log"), "a", 0o600);
139
+ try {
140
+ const runner = fileURLToPath(new URL(import.meta.url.endsWith(".ts") ? "./runner.ts" : "./runner.js", import.meta.url));
141
+ child = spawn(process.execPath, [runner, base, launch.rootId, token, launch.id], {
142
+ cwd: launch.cwd,
143
+ env: childEnvironment(launch),
144
+ detached: true,
145
+ stdio: ["ignore", log, log],
146
+ });
147
+ if (child.pid === undefined) {
148
+ spawnError = new Promise((resolve) => {
149
+ child.once("error", (error) => resolve(saveSpawnFailure(dir, error)));
150
+ });
151
+ claim.release();
152
+ }
153
+ else {
154
+ claim.spawned();
155
+ child.on("error", (error) => {
156
+ write(path.join(dir, "spawn-error.json"), { error: error.message });
157
+ });
158
+ }
159
+ }
160
+ finally {
161
+ fs.closeSync(log);
162
+ }
163
+ }
164
+ catch (error) {
165
+ claim.release();
166
+ throw error;
134
167
  }
135
- finally {
136
- fs.closeSync(log);
168
+ if (child.pid === undefined && spawnError) {
169
+ throw await spawnError;
170
+ }
171
+ if (child.pid === undefined) {
172
+ claim.spawned();
173
+ throw new Error("Actor spawn outcome is ambiguous.");
137
174
  }
138
- child.on("error", (error) => {
139
- write(path.join(dir, "spawn-error.json"), { error: error.message });
140
- });
141
175
  child.on("exit", (code, signal) => {
142
176
  write(path.join(dir, `exit-${token}.json`), { pid: child.pid, code, signal, at: Date.now() });
143
177
  trySignalChildTree(child, "SIGKILL");
144
178
  });
145
179
  child.unref();
146
- if (child.pid === undefined) {
147
- throw new Error("Actor was not spawned; retained launch claim requires inspection.");
148
- }
149
180
  write(path.join(dir, "actor.lock", "process.json"), {
150
181
  pid: child.pid,
151
182
  identity: readChildProcessIdentity(child.pid) ?? "unverified",
@@ -170,6 +201,48 @@ async function awaitStopping(dir, existing, signal) {
170
201
  }
171
202
  }
172
203
  }
204
+ function releaseClaim(lock, identity, token) {
205
+ try {
206
+ const current = fs.lstatSync(lock, { throwIfNoEntry: false });
207
+ if (current && identity && current.dev === identity.dev && current.ino === identity.ino) {
208
+ const file = path.join(lock, "claim.json");
209
+ const claim = read(file, processSchema);
210
+ if (claim === undefined || claim.token === token) {
211
+ fs.rmSync(file, { force: true });
212
+ fs.rmdirSync(lock);
213
+ }
214
+ }
215
+ }
216
+ catch {
217
+ // Keep the original failure; an unremovable claim remains safely ambiguous.
218
+ }
219
+ }
220
+ async function launchClaimedActor(base, launch, lock) {
221
+ let identity;
222
+ let token;
223
+ let state = "claimed";
224
+ const claim = {
225
+ spawned: () => {
226
+ state = "spawned";
227
+ },
228
+ release: () => {
229
+ if (state === "claimed") {
230
+ state = "released";
231
+ releaseClaim(lock, identity, token);
232
+ }
233
+ },
234
+ };
235
+ try {
236
+ identity = fs.lstatSync(lock);
237
+ token = randomUUID();
238
+ write(path.join(lock, "claim.json"), { ...ownIdentity(), token });
239
+ await spawnActor(base, launch, token, claim);
240
+ }
241
+ catch (error) {
242
+ claim.release();
243
+ throw error;
244
+ }
245
+ }
173
246
  export async function ensureActor(base, launch, signal) {
174
247
  const dir = conversation(base, launch);
175
248
  const lock = path.join(dir, "actor.lock");
@@ -199,16 +272,26 @@ export async function ensureActor(base, launch, signal) {
199
272
  }
200
273
  throw error;
201
274
  }
202
- const token = randomUUID();
203
- write(path.join(lock, "claim.json"), { ...ownIdentity(), token });
204
- spawnActor(base, launch, token);
275
+ await launchClaimedActor(base, launch, lock);
276
+ }
277
+ /** A failed restore can hand the old cut forward only before native open and after death. */
278
+ function failedBeforeOpenToken(dir, exited, current) {
279
+ const failure = read(path.join(dir, `wake-failure-${exited.token}.json`), wakeFailureSchema);
280
+ if (current?.token === exited.token &&
281
+ !alive(current) &&
282
+ failure?.beforeOpen?.actorToken === exited.token) {
283
+ return failure.beforeOpen.parkedToken;
284
+ }
285
+ return undefined;
205
286
  }
206
287
  /** A failed park keeps its input queued; exact state is lost, so nothing restarts it implicitly. */
207
288
  export function parkFailure(base, launch, exited) {
208
289
  const dir = conversation(base, launch);
209
290
  const parked = read(path.join(dir, "parked.json"), parkedSchema);
210
- if (exited && parked && parked.token !== exited.token) {
211
- // Only the exited claim that saved this state may hand it to the next generation.
291
+ if (exited &&
292
+ parked &&
293
+ parked.token !== exited.token &&
294
+ failedBeforeOpenToken(dir, exited, actor(base, launch)) !== parked.token) {
212
295
  throw new Error("Parked working state belongs to another actor generation; nothing was reopened.");
213
296
  }
214
297
  const file = path.join(dir, "park-failed.json");
@@ -307,45 +390,6 @@ export function requestStop(base, launch, onError) {
307
390
  });
308
391
  return { targets: live, command };
309
392
  }
310
- export async function stopChildren(base, owner) {
311
- const roots = new Set();
312
- const targets = [];
313
- for (const id of list(base)) {
314
- try {
315
- const launch = launchAt(base, id, owner);
316
- if (!roots.has(launch.rootId)) {
317
- roots.add(launch.rootId);
318
- targets.push(...requestStop(base, launch, (error) => {
319
- console.error(`Child shutdown ${id}: ${errorText(error)}`);
320
- }).targets);
321
- }
322
- }
323
- catch (error) {
324
- console.error(`Child shutdown ${id}: ${errorText(error)}`);
325
- }
326
- }
327
- await waitForExit(targets, 3000);
328
- for (const target of targets) {
329
- try {
330
- signalProcess(target, "SIGKILL");
331
- }
332
- catch (error) {
333
- console.error(`Child shutdown ${target.pid}: ${errorText(error)}`);
334
- }
335
- }
336
- await waitForExit(targets, 1000);
337
- if (targets.some(alive)) {
338
- console.error("Child shutdown could not confirm every owned process exit.");
339
- }
340
- }
341
- async function waitForExit(targets, timeout) {
342
- const deadline = Date.now() + timeout;
343
- while (Date.now() < deadline && targets.some(alive)) {
344
- // Join actual process exit while native abort persists the interrupted turn.
345
- // oxlint-disable-next-line no-await-in-loop
346
- await delay(25);
347
- }
348
- }
349
393
  export function shell(file, token) {
350
394
  const identity = published(file, read(file, processSchema));
351
395
  if (identity && identity.token !== token) {
@@ -135,13 +135,30 @@ function owned(file, owner) {
135
135
  const header = journal.entries.at(0);
136
136
  return header?.type === "session" && header.id === owner ? journal : undefined;
137
137
  }
138
+ /** Without a session file (`--no-session`), Pi's append-only memory is the whole native transcript. */
139
+ function remembered(manager, owner) {
140
+ const header = manager.getHeader();
141
+ if (header?.id !== owner) {
142
+ return;
143
+ }
144
+ const journal = { end: 0, entries: [], completions: new Map() };
145
+ // Index the same record frames the physical journal scan accepts.
146
+ for (const entry of [header, ...manager.getEntries()]) {
147
+ if (isRecord(entry)) {
148
+ addEntry(journal, entry);
149
+ }
150
+ }
151
+ return journal;
152
+ }
138
153
  export function receiptId(file, owner, matches) {
139
154
  const found = owned(file, owner)?.entries.find((entry) => entry.type !== "session" && matches(entry));
140
155
  return typeof found?.id === "string" ? found.id : undefined;
141
156
  }
142
- /** Live read-only completion IDs by run; later appends extend this view.
143
- * A matching entry without an ID is unpublished. Invalidations replace the view.
157
+ /** Completion IDs by run from the native transcript: the session file when Pi persists one,
158
+ * otherwise Pi's in-memory entries. A matching entry without an ID is unpublished.
144
159
  */
145
- export function completionReceipts(file, owner) {
146
- return owned(file, owner)?.completions ?? new Map();
160
+ export function completionReceipts(manager, owner) {
161
+ const file = manager.getSessionFile();
162
+ const journal = file === undefined ? remembered(manager, owner) : owned(file, owner);
163
+ return journal?.completions ?? new Map();
147
164
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { commandSchema } from "./contracts.js";
3
+ import { commandSchema, parkedSchema, } from "./contracts.js";
4
4
  import { launchAt, missing, read, runDirectory, write } from "./storage.js";
5
5
  import { ownIdentity, ParentCheck, shells, signalProcess } from "./process.js";
6
6
  import { errorText, loadSDK } from "./host.js";
@@ -351,7 +351,7 @@ class Actor {
351
351
  }
352
352
  return this.parking;
353
353
  }
354
- failure(error) {
354
+ failure(error, unopened) {
355
355
  const launch = this.current ?? this.initial;
356
356
  const result = {
357
357
  id: launch.id,
@@ -361,6 +361,16 @@ class Actor {
361
361
  error: errorText(error),
362
362
  };
363
363
  const directory = runDirectory(this.base, launch.id);
364
+ if (fs.existsSync(path.join(this.dir, "parked.json"))) {
365
+ const parked = read(path.join(this.dir, "parked.json"), parkedSchema);
366
+ write(path.join(this.dir, `wake-failure-${this.token}.json`), {
367
+ at: Date.now(),
368
+ reason: errorText(error),
369
+ ...(unopened !== undefined && parked?.token === unopened
370
+ ? { beforeOpen: { actorToken: this.token, parkedToken: unopened } }
371
+ : {}),
372
+ });
373
+ }
364
374
  if (!fs.existsSync(path.join(directory, "result.json"))) {
365
375
  fs.writeFileSync(path.join(directory, "output.txt"), "", { mode: 0o600 });
366
376
  write(path.join(directory, "result.json"), result);
@@ -427,10 +437,14 @@ class Actor {
427
437
  parked.adopt();
428
438
  }
429
439
  async serve() {
430
- const sdk = await loadSDK(this.initial.sdk);
431
- const parked = new Parking(this.base, this.initial, this.token, sdk);
432
440
  let runtime;
441
+ let parked;
442
+ let parkedToken;
433
443
  try {
444
+ // Capture the old generation before SDK import or any native working-state operation.
445
+ parkedToken = read(path.join(this.dir, "parked.json"), parkedSchema)?.token;
446
+ const sdk = await loadSDK(this.initial.sdk);
447
+ parked = new Parking(this.base, this.initial, this.token, sdk);
434
448
  runtime = await this.start(sdk, parked);
435
449
  process.on("SIGTERM", this.stop);
436
450
  process.on("SIGINT", this.stop);
@@ -438,7 +452,7 @@ class Actor {
438
452
  await this.active;
439
453
  }
440
454
  catch (error) {
441
- this.failure(error);
455
+ this.failure(error, parked?.opened === true ? undefined : parkedToken);
442
456
  }
443
457
  finally {
444
458
  await this.finish(runtime);
@@ -0,0 +1,43 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import { errorText } from "./host.js";
3
+ import { alive, requestStop, signalProcess } from "./process.js";
4
+ import { launchAt, list } from "./storage.js";
5
+ export async function stopChildren(base, owner, report) {
6
+ const roots = new Set();
7
+ const targets = [];
8
+ for (const id of list(base)) {
9
+ try {
10
+ const launch = launchAt(base, id, owner);
11
+ if (!roots.has(launch.rootId)) {
12
+ roots.add(launch.rootId);
13
+ targets.push(...requestStop(base, launch, (error) => {
14
+ report(`Child shutdown ${id}: ${errorText(error)}`);
15
+ }).targets);
16
+ }
17
+ }
18
+ catch (error) {
19
+ report(`Child shutdown ${id}: ${errorText(error)}`);
20
+ }
21
+ }
22
+ await waitForExit(targets, 3000);
23
+ for (const target of targets) {
24
+ try {
25
+ signalProcess(target, "SIGKILL");
26
+ }
27
+ catch (error) {
28
+ report(`Child shutdown ${target.pid}: ${errorText(error)}`);
29
+ }
30
+ }
31
+ await waitForExit(targets, 1000);
32
+ if (targets.some(alive)) {
33
+ report("Child shutdown could not confirm every owned process exit.");
34
+ }
35
+ }
36
+ async function waitForExit(targets, timeout) {
37
+ const deadline = Date.now() + timeout;
38
+ while (Date.now() < deadline && targets.some(alive)) {
39
+ // Join actual process exit while native abort persists the interrupted turn.
40
+ // oxlint-disable-next-line no-await-in-loop
41
+ await delay(25);
42
+ }
43
+ }
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { readOutputPage } from "../shared/output-page.js";
4
4
  import { actor, alive, observeAlive } from "./process.js";
5
- import { admissionSchema, commandSchema, exitSchema, successorSchema, } from "./contracts.js";
5
+ import { admissionSchema, commandSchema, exitSchema, successorSchema, wakeFailureSchema, } from "./contracts.js";
6
6
  import { conversation, read, runDirectory } from "./storage.js";
7
7
  import { published } from "./notifications.js";
8
8
  import { readCanonicalResult, readResult } from "./usage.js";
@@ -98,6 +98,7 @@ function metadata(base, launch, result, options) {
98
98
  result,
99
99
  nativeReceipt: options.ctx ? (published(options.ctx, launch) ?? null) : null,
100
100
  process,
101
+ lastWakeFailure: read(path.join(conversation(base, launch), "wake-failure.json"), wakeFailureSchema),
101
102
  sessionId: launch.sessionId,
102
103
  sessionFile: launch.sessionFile,
103
104
  outputFile,
@@ -178,6 +179,9 @@ function resultText(details) {
178
179
  `Agent run ${details.id}: ${details.status}; admission ${details.admission}; process ${details.process.live ? "live" : "not observed live"}.`,
179
180
  receiptText(details),
180
181
  exitText(details),
182
+ details.lastWakeFailure
183
+ ? `Last wake failure at ${new Date(details.lastWakeFailure.at).toISOString()}: ${details.lastWakeFailure.reason} Retained intercom messages may still arrive after a later wake.`
184
+ : "",
181
185
  `Guidance (latest 20):\n${guidance.length > 0 ? guidance : " none"}`,
182
186
  "Guidance states: pending = not yet admitted; accepted = native queue admission, not journal publication or model consumption; stale = rejected, no new work was started (retry the entire prompt as a continuation).",
183
187
  details.result?.error ?? "",
@@ -1 +1 @@
1
- export const EXTENSION_BUILD = Object.freeze({"version":"0.52.4","sha256":"d8c708fa06701cc0d5f8770cc729ba6fa204bdc8f76c5f06f5aa9cc3e37b3cc9"});
1
+ export const EXTENSION_BUILD = Object.freeze({"version":"0.52.5","sha256":"ccb2b2cb4725bfa7f5e813494a2007dc60ed9c04e677502d08df23612cc30af8"});
@@ -71,9 +71,7 @@ class IntercomBroker {
71
71
  if (currentId === null && msg.type !== "register") {
72
72
  throw new Error(`Received ${msg.type} before register`);
73
73
  }
74
- if (currentId !== null) {
75
- this.touchActivity(currentId, false);
76
- }
74
+ this.touchActivity(currentId, false);
77
75
  if (msg.type === "register") {
78
76
  if (currentId !== null) {
79
77
  throw new Error("Received duplicate register message");
@@ -107,7 +105,8 @@ class IntercomBroker {
107
105
  break;
108
106
  case "park":
109
107
  case "unpark":
110
- this.parking.park(socket, currentId, msg.type === "park");
108
+ case "wake_result":
109
+ this.parking.handle(socket, currentId, msg);
111
110
  break;
112
111
  default:
113
112
  throw new Error(`Unknown client message type: ${msg.type}`);
@@ -126,7 +125,7 @@ class IntercomBroker {
126
125
  topics: resumed?.topics ?? new TopicSnapshot(info),
127
126
  topicFrames,
128
127
  lastMessages: msg.lastMessages === true,
129
- parking: msg.parking === true,
128
+ ...this.parking.features(msg),
130
129
  receiptReads: msg.receiptReads === 1,
131
130
  });
132
131
  setId(id);
@@ -139,6 +138,7 @@ class IntercomBroker {
139
138
  sessionId: id,
140
139
  lastMessages: true,
141
140
  parking: msg.parking === true,
141
+ ...this.parking.capabilities(msg),
142
142
  receiptReads: 1,
143
143
  ...(msg.guardedDelivery === 1 ? { guardedDelivery: 1 } : {}),
144
144
  ...(topicFrames ? { topicsSupported: true, topicFrames: true } : {}),
@@ -331,12 +331,13 @@ class IntercomBroker {
331
331
  accepted: reason === null,
332
332
  delivered: reason === null && !queued,
333
333
  ...(queued ? { queued } : {}),
334
+ ...(queued ? { reason: this.parking.queueReason(target, message.id) } : {}),
334
335
  ...(reason !== null ? { reason } : {}),
335
336
  };
336
337
  }
337
338
  /** Update liveness/intercom-activity timestamps for a connected session. */
338
339
  touchActivity(sessionId, comms) {
339
- const session = this.sessions.get(sessionId);
340
+ const session = sessionId === null ? undefined : this.sessions.get(sessionId);
340
341
  if (!session) {
341
342
  return;
342
343
  }
@@ -0,0 +1,17 @@
1
+ import type net from "node:net";
2
+ import type { EventEmitter } from "node:events";
3
+ import type { WakeResult } from "../types.ts";
4
+ import type { UnknownRecord } from "../../shared/unknown.ts";
5
+ /** Negotiated wake outcomes; old brokers and clients keep the original wake event. */
6
+ export declare class WakeMessages {
7
+ supported: boolean;
8
+ private readonly events;
9
+ private readonly socket;
10
+ constructor(events: EventEmitter, socket: () => net.Socket);
11
+ registration(): {
12
+ wakeResults?: 1;
13
+ wakeNotices?: 1;
14
+ };
15
+ receive(msg: UnknownRecord): void;
16
+ respond(requestId: string, result: WakeResult): void;
17
+ }
@@ -0,0 +1,54 @@
1
+ import { validateIntercomMessageSize, writeMessage } from "./framing.js";
2
+ /** Negotiated wake outcomes; old brokers and clients keep the original wake event. */
3
+ export class WakeMessages {
4
+ supported = false;
5
+ events;
6
+ socket;
7
+ constructor(events, socket) {
8
+ this.events = events;
9
+ this.socket = socket;
10
+ }
11
+ registration() {
12
+ return {
13
+ ...(this.events.listenerCount("wake_request") > 0 ? { wakeResults: 1 } : {}),
14
+ ...(this.events.listenerCount("wake_failed") > 0 ? { wakeNotices: 1 } : {}),
15
+ };
16
+ }
17
+ receive(msg) {
18
+ if (msg.type === "wake_registered") {
19
+ if (typeof msg.requestId !== "string") {
20
+ throw new Error("Invalid wake registration");
21
+ }
22
+ this.events.emit("wake_registered", msg.requestId);
23
+ return;
24
+ }
25
+ if (typeof msg.sessionId !== "string") {
26
+ throw new Error("Invalid wake message");
27
+ }
28
+ if (msg.type === "wake") {
29
+ if (this.supported && typeof msg.requestId === "string") {
30
+ this.events.emit("wake_request", msg.sessionId, msg.requestId);
31
+ }
32
+ else {
33
+ this.events.emit("wake", msg.sessionId);
34
+ }
35
+ return;
36
+ }
37
+ if (typeof msg.messageId !== "string" || typeof msg.reason !== "string") {
38
+ throw new Error("Invalid wake failure");
39
+ }
40
+ this.events.emit("wake_failed", msg.sessionId, msg.messageId, msg.reason);
41
+ }
42
+ respond(requestId, result) {
43
+ if (!this.supported) {
44
+ return;
45
+ }
46
+ const frame = { type: "wake_result", requestId, result };
47
+ writeMessage(this.socket(), validateIntercomMessageSize(frame) === null
48
+ ? frame
49
+ : {
50
+ ...frame,
51
+ result: { status: "failed", reason: "Wake failed; inspect the agent run for details." },
52
+ });
53
+ }
54
+ }
@@ -1,4 +1,5 @@
1
1
  import { EventEmitter } from "events";
2
+ import { WakeMessages } from "./client-wake.ts";
2
3
  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
4
  export type { SendResult } from "../types.ts";
4
5
  export interface IntercomClientOptions {
@@ -12,6 +13,7 @@ export declare class IntercomClient extends EventEmitter {
12
13
  private _sessionId;
13
14
  private _topicsSupported;
14
15
  private readonly parking;
16
+ readonly wakes: WakeMessages;
15
17
  private guardedDeliverySupported;
16
18
  private readonly lastRequests;
17
19
  private readonly pendingSends;
@@ -5,6 +5,7 @@ import { BrokerConnection, connectBrokerSocket } from "./connection.js";
5
5
  import { SnapshotReader } from "./snapshot-reader.js";
6
6
  import { LastMessageRequests, lastRequest } from "./client-last.js";
7
7
  import { ParkRequests } from "./client-park.js";
8
+ import { WakeMessages } from "./client-wake.js";
8
9
  import { isNativeReadResult } from "./last.js";
9
10
  import { errorMessage, isRecord, isUnknownArray, } from "../../shared/unknown.js";
10
11
  import { compactTopicMessage, normalizeMessage, normalizeSessionInfo, } from "../types.js";
@@ -32,6 +33,7 @@ export class IntercomClient extends EventEmitter {
32
33
  _sessionId = null;
33
34
  _topicsSupported = false;
34
35
  parking = new ParkRequests();
36
+ wakes = new WakeMessages(this, () => this.requireActiveSocket());
35
37
  guardedDeliverySupported = false;
36
38
  lastRequests;
37
39
  pendingSends = new Map();
@@ -106,6 +108,7 @@ export class IntercomClient extends EventEmitter {
106
108
  this._topicsSupported = false;
107
109
  this.parking.settle(false);
108
110
  this.guardedDeliverySupported = false;
111
+ this.wakes.supported = false;
109
112
  this.disconnecting = false;
110
113
  this.failPending(error);
111
114
  if (established && !expected) {
@@ -125,6 +128,7 @@ export class IntercomClient extends EventEmitter {
125
128
  parking: true,
126
129
  receiptReads: 1,
127
130
  guardedDelivery: 1,
131
+ ...this.wakes.registration(),
128
132
  });
129
133
  }
130
134
  catch (error) {
@@ -157,44 +161,46 @@ export class IntercomClient extends EventEmitter {
157
161
  if (!isRecord(msg) || typeof msg.type !== "string") {
158
162
  throw new Error("Invalid broker message");
159
163
  }
160
- const brokerMessage = msg;
161
- const type = msg.type;
162
- if (this._sessionId === null && type !== "registered") {
163
- throw new Error(`Received ${type} before registered`);
164
+ if (this._sessionId === null && msg.type !== "registered") {
165
+ throw new Error(`Received ${msg.type} before registered`);
164
166
  }
165
- switch (type) {
167
+ switch (msg.type) {
166
168
  case "registered":
167
- this.receiveRegistration(brokerMessage);
169
+ this.receiveRegistration(msg);
168
170
  break;
169
171
  case "sessions":
170
- this.receiveSessions(brokerMessage);
172
+ this.receiveSessions(msg);
171
173
  break;
172
174
  case "last_request":
173
- this.receiveLastRequest(brokerMessage);
175
+ this.receiveLastRequest(msg);
174
176
  break;
175
177
  case "last_response":
176
- this.lastRequests.receive(brokerMessage);
178
+ this.lastRequests.receive(msg);
177
179
  break;
178
180
  case "message":
179
- this.receiveMessage(brokerMessage);
181
+ this.receiveMessage(msg);
180
182
  break;
181
183
  case "delivered":
182
184
  case "delivery_queued":
183
185
  case "delivery_failed":
184
- this.receiveDelivery(brokerMessage);
186
+ this.receiveDelivery(msg);
185
187
  break;
186
188
  case "parked":
187
189
  this.parking.settle(true);
188
190
  break;
189
- case "wake":
190
191
  case "session_left":
191
- if (typeof brokerMessage.sessionId !== "string") {
192
- throw new Error(`Invalid ${type} message`);
192
+ if (typeof msg.sessionId !== "string") {
193
+ throw new Error(`Invalid ${msg.type} message`);
193
194
  }
194
- this.emit(type, brokerMessage.sessionId);
195
+ this.emit(msg.type, msg.sessionId);
196
+ break;
197
+ case "wake":
198
+ case "wake_failed":
199
+ case "wake_registered":
200
+ this.wakes.receive(msg);
195
201
  break;
196
202
  default:
197
- throw new Error(`Unknown broker message type: ${type}`);
203
+ throw new Error(`Unknown broker message type: ${msg.type}`);
198
204
  }
199
205
  }
200
206
  receiveRegistration(msg) {
@@ -210,6 +216,7 @@ export class IntercomClient extends EventEmitter {
210
216
  this.parking.supported = msg.parking === true;
211
217
  this.lastRequests.receiptReads = msg.receiptReads === 1;
212
218
  this.guardedDeliverySupported = msg.guardedDelivery === 1;
219
+ this.wakes.supported = msg.wakeResults === 1;
213
220
  this.connection?.confirmRegistration();
214
221
  this.emit("_registered", { type: "registered", sessionId: msg.sessionId });
215
222
  }