@fitchmultz/pi-subagents 0.52.19 → 0.52.21

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 (56) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/agent/contracts.js +12 -3
  3. package/dist/agent/index.js +9 -9
  4. package/dist/agent/legacy.js +5 -5
  5. package/dist/agent/notifications.js +21 -14
  6. package/dist/agent/parent-check.js +38 -0
  7. package/dist/agent/process.js +45 -52
  8. package/dist/agent/runner.js +59 -64
  9. package/dist/agent/storage.js +16 -4
  10. package/dist/agent/view.js +1 -3
  11. package/dist/build-info.js +1 -1
  12. package/dist/pi-intercom/bridge-presence.js +30 -18
  13. package/dist/pi-intercom/bridge-wake.js +60 -37
  14. package/dist/pi-intercom/broker/broker.js +4 -26
  15. package/dist/pi-intercom/broker/client-last.js +6 -18
  16. package/dist/pi-intercom/broker/client-park.d.ts +1 -1
  17. package/dist/pi-intercom/broker/client-park.js +22 -6
  18. package/dist/pi-intercom/broker/client.d.ts +3 -2
  19. package/dist/pi-intercom/broker/client.js +104 -112
  20. package/dist/pi-intercom/broker/connection.d.ts +3 -5
  21. package/dist/pi-intercom/broker/connection.js +48 -33
  22. package/dist/pi-intercom/broker/framing.d.ts +1 -1
  23. package/dist/pi-intercom/broker/framing.js +19 -12
  24. package/dist/pi-intercom/broker/pending-requests.d.ts +12 -0
  25. package/dist/pi-intercom/broker/pending-requests.js +26 -0
  26. package/dist/pi-intercom/broker/replace-deliveries.js +38 -0
  27. package/dist/pi-intercom/broker/snapshot-reader.d.ts +3 -2
  28. package/dist/pi-intercom/broker/snapshot-reader.js +34 -15
  29. package/dist/pi-intercom/connection.js +5 -8
  30. package/dist/pi-intercom/inbound-journal.js +5 -2
  31. package/dist/shared/journal-frames.js +19 -24
  32. package/docs/development.md +6 -0
  33. package/package.json +1 -1
  34. package/src/agent/contracts.ts +12 -3
  35. package/src/agent/index.ts +10 -10
  36. package/src/agent/legacy.ts +5 -5
  37. package/src/agent/notifications.ts +35 -15
  38. package/src/agent/parent-check.ts +40 -0
  39. package/src/agent/process.ts +53 -55
  40. package/src/agent/runner.ts +65 -65
  41. package/src/agent/storage.ts +18 -5
  42. package/src/agent/view.ts +1 -4
  43. package/src/pi-intercom/bridge-presence.ts +37 -18
  44. package/src/pi-intercom/bridge-wake.ts +70 -38
  45. package/src/pi-intercom/broker/broker.ts +5 -37
  46. package/src/pi-intercom/broker/client-last.ts +14 -25
  47. package/src/pi-intercom/broker/client-park.ts +27 -6
  48. package/src/pi-intercom/broker/client.ts +127 -126
  49. package/src/pi-intercom/broker/connection.ts +58 -39
  50. package/src/pi-intercom/broker/framing.ts +24 -12
  51. package/src/pi-intercom/broker/pending-requests.ts +37 -0
  52. package/src/pi-intercom/broker/replace-deliveries.ts +56 -0
  53. package/src/pi-intercom/broker/snapshot-reader.ts +44 -16
  54. package/src/pi-intercom/connection.ts +9 -9
  55. package/src/pi-intercom/inbound-journal.ts +5 -2
  56. package/src/shared/journal-frames.ts +22 -23
@@ -2,12 +2,10 @@ 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, wakeFailureSchema, } from "./contracts.js";
5
+ import { admissionSchema, commandMetadataSchema, 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 { accountingFields, accountingState, readCanonicalResult, readResult } from "./usage.js";
9
- import { Type } from "./typebox.js";
10
- const commandMetadataSchema = Type.Pick(commandSchema, ["id", "runId", "action", "createdAt"]);
11
9
  function toolResult(value, text) {
12
10
  return { content: [{ type: "text", text }], details: value };
13
11
  }
@@ -1 +1 @@
1
- export const EXTENSION_BUILD = Object.freeze({"version":"0.52.19","sha256":"a5cb823f7d9194659bb3c3e53c68e90032ef520a962758d896dccee78a3bd2c5"});
1
+ export const EXTENSION_BUILD = Object.freeze({"version":"0.52.21","sha256":"d0c9a96a0eca06a58dc73d77b64300d7e41a9eb5c75b1deee8751ae3af4b87ce"});
@@ -4,9 +4,7 @@ import { readBridgeFile, writeBridgeJson } from "./bridge-files.js";
4
4
  import { log } from "./bridge-protocol.js";
5
5
  export class PersistentPresence {
6
6
  registrations = new Map();
7
- stopped = false;
8
- timer;
9
- running;
7
+ phase = { kind: "idle" };
10
8
  owner;
11
9
  constructor(owner) {
12
10
  this.owner = owner;
@@ -61,21 +59,28 @@ export class PersistentPresence {
61
59
  this.start();
62
60
  }
63
61
  start() {
64
- if (!this.stopped && !this.running) {
65
- clearTimeout(this.timer);
66
- this.timer = setTimeout(() => {
67
- this.running = this.maintain().finally(() => {
68
- this.running = undefined;
69
- if (!this.stopped) {
70
- this.timer = setTimeout(() => this.start(), 15000);
71
- }
72
- });
73
- }, 0);
62
+ if (this.phase.kind === "running" || this.phase.kind === "stopped") {
63
+ return;
64
+ }
65
+ this.schedule(0);
66
+ }
67
+ schedule(delay) {
68
+ if (this.phase.kind === "scheduled") {
69
+ clearTimeout(this.phase.timer);
74
70
  }
71
+ const timer = setTimeout(() => {
72
+ const task = this.maintain().finally(() => {
73
+ if (this.phase.kind !== "stopped") {
74
+ this.schedule(15000);
75
+ }
76
+ });
77
+ this.phase = { kind: "running", task };
78
+ }, delay);
79
+ this.phase = { kind: "scheduled", timer };
75
80
  }
76
81
  async maintain() {
77
82
  for (const registration of this.registrations.values()) {
78
- if (this.stopped) {
83
+ if (this.phase.kind === "stopped") {
79
84
  return;
80
85
  }
81
86
  const identity = this.owner.clients().get(registration.fingerprint);
@@ -95,9 +100,16 @@ export class PersistentPresence {
95
100
  }
96
101
  }
97
102
  }
98
- async stop() {
99
- this.stopped = true;
100
- clearTimeout(this.timer);
101
- await this.running;
103
+ stop() {
104
+ const phase = this.phase;
105
+ if (phase.kind === "stopped") {
106
+ return phase.task;
107
+ }
108
+ if (phase.kind === "scheduled") {
109
+ clearTimeout(phase.timer);
110
+ }
111
+ const task = phase.kind === "running" ? phase.task : Promise.resolve();
112
+ this.phase = { kind: "stopped", task };
113
+ return task;
102
114
  }
103
115
  }
@@ -22,9 +22,7 @@ export class WakeDeliveries {
22
22
  allowed;
23
23
  deliveries = new Map();
24
24
  controller = new AbortController();
25
- timer;
26
- running;
27
- dirty = false;
25
+ phase = { kind: "idle" };
28
26
  storageBackoff = 15000;
29
27
  constructor(stateDir, allowed) {
30
28
  this.stateDir = stateDir;
@@ -62,11 +60,17 @@ export class WakeDeliveries {
62
60
  catch {
63
61
  // The scheduled storage recovery fails closed; never let an event bypass durable admission.
64
62
  }
65
- if (this.running) {
66
- this.dirty = true;
67
- }
68
- else {
69
- this.schedule(Date.now());
63
+ const phase = this.phase;
64
+ switch (phase.kind) {
65
+ case "running":
66
+ this.phase = { kind: "running-dirty", task: phase.task };
67
+ return;
68
+ case "running-dirty":
69
+ case "stopped":
70
+ return;
71
+ case "idle":
72
+ case "scheduled":
73
+ this.schedule(Date.now());
70
74
  }
71
75
  }
72
76
  reconcile() {
@@ -80,28 +84,36 @@ export class WakeDeliveries {
80
84
  }
81
85
  }
82
86
  schedule(deadline) {
83
- clearTimeout(this.timer);
84
- this.timer = setTimeout(() => {
85
- this.dirty = false;
86
- let next = Infinity;
87
- this.running = this.deliver()
88
- .then((due) => {
89
- next = due;
90
- this.storageBackoff = 15000;
87
+ if (this.phase.kind === "scheduled") {
88
+ clearTimeout(this.phase.timer);
89
+ }
90
+ const timer = setTimeout(() => this.startDelivery(), Math.max(0, Math.min(deadline - Date.now(), 0x7fffffff)));
91
+ this.phase = { kind: "scheduled", timer };
92
+ }
93
+ startDelivery() {
94
+ let next = Infinity;
95
+ const task = this.deliver()
96
+ .then((due) => {
97
+ next = due;
98
+ this.storageBackoff = 15000;
99
+ return;
100
+ })
101
+ .catch(() => {
102
+ log("wake_delivery", { result: "storage_unavailable" });
103
+ next = Date.now() + this.storageBackoff;
104
+ this.storageBackoff = Math.min(this.storageBackoff * 2, 300000);
105
+ })
106
+ .finally(() => {
107
+ if (this.phase.kind === "stopped") {
91
108
  return;
92
- })
93
- .catch(() => {
94
- log("wake_delivery", { result: "storage_unavailable" });
95
- next = Date.now() + this.storageBackoff;
96
- this.storageBackoff = Math.min(this.storageBackoff * 2, 300000);
97
- })
98
- .finally(() => {
99
- this.running = undefined;
100
- if (!this.controller.signal.aborted && (this.dirty || Number.isFinite(next))) {
101
- this.schedule(this.dirty ? Date.now() : next);
102
- }
103
- });
104
- }, Math.max(0, Math.min(deadline - Date.now(), 0x7fffffff)));
109
+ }
110
+ const dirty = this.phase.kind === "running-dirty";
111
+ this.phase = { kind: "idle" };
112
+ if (dirty || Number.isFinite(next)) {
113
+ this.schedule(dirty ? Date.now() : next);
114
+ }
115
+ });
116
+ this.phase = { kind: "running", task };
105
117
  }
106
118
  authorized(delivery) {
107
119
  return (!this.controller.signal.aborted &&
@@ -233,14 +245,25 @@ export class WakeDeliveries {
233
245
  }
234
246
  this.changed(false);
235
247
  }
236
- async stop() {
237
- this.controller.abort();
238
- clearTimeout(this.timer);
239
- await this.running;
240
- for (const delivery of this.deliveries.values()) {
241
- delivery.mailbox.close();
248
+ stop() {
249
+ const phase = this.phase;
250
+ if (phase.kind === "stopped") {
251
+ return phase.task;
252
+ }
253
+ if (phase.kind === "scheduled") {
254
+ clearTimeout(phase.timer);
242
255
  }
243
- this.deliveries.clear();
244
- this.state?.close();
256
+ const running = phase.kind === "running" || phase.kind === "running-dirty" ? phase.task : undefined;
257
+ const task = Promise.resolve(running).then(() => {
258
+ for (const delivery of this.deliveries.values()) {
259
+ delivery.mailbox.close();
260
+ }
261
+ this.deliveries.clear();
262
+ this.state?.close();
263
+ return;
264
+ });
265
+ this.phase = { kind: "stopped", task };
266
+ this.controller.abort();
267
+ return task;
245
268
  }
246
269
  }
@@ -14,13 +14,13 @@ import { guardedDeliveryError } from "./guard.js";
14
14
  import { compactTopicMessage, normalizeMessage, normalizeSessionInfo, } from "../types.js";
15
15
  import { errorMessage, isRecord } from "../../shared/unknown.js";
16
16
  import { messageSender, snapshotFrames, TopicSnapshot, publicationMessage, preparedPublication, deliveryAcknowledgement, shouldReplaceDelivery, } from "./snapshots.js";
17
+ import { ReplaceDeliveries } from "./replace-deliveries.js";
17
18
  const PID_PATH = join(getBrokerRuntimeDir(), "broker.pid");
18
- const REPLACE_DELIVERY_DELAY_MS = 1500;
19
19
  class IntercomBroker {
20
20
  sessions = new Map();
21
21
  lastMessages = new LastMessageRelay(this.sessions);
22
22
  parking = new ParkedSessions(this.sessions, this.deliverMessage.bind(this));
23
- pendingReplaceDeliveries = new Map();
23
+ replacements = new ReplaceDeliveries(this.deliverMessage.bind(this));
24
24
  server;
25
25
  shutdownTimer = null;
26
26
  socketPath;
@@ -153,7 +153,7 @@ class IntercomBroker {
153
153
  this.lastMessages.disconnected(sessionId);
154
154
  if (!this.parking.closed(session)) {
155
155
  this.sessions.delete(sessionId);
156
- this.clearPendingReplaceDeliveries(sessionId);
156
+ this.replacements.disconnected(sessionId);
157
157
  this.broadcast({ type: "session_left", sessionId }, session.info);
158
158
  }
159
159
  this.scheduleShutdownCheck();
@@ -348,25 +348,12 @@ class IntercomBroker {
348
348
  ...(comms ? { lastIntercomActivity: now } : {}),
349
349
  };
350
350
  }
351
- replaceKey(fromId, toId, threadId) {
352
- return `${fromId}\0${toId}\0${threadId}`;
353
- }
354
351
  queueReplaceDelivery(fromId, toId, from, message) {
355
352
  const validationError = this.validateDeliveryPayload(from, message, this.sessions.get(toId));
356
353
  if (validationError !== null) {
357
354
  return { id: message.id, accepted: false, delivered: false, reason: validationError };
358
355
  }
359
- const key = this.replaceKey(fromId, toId, message.threadId ?? "");
360
- const existing = this.pendingReplaceDeliveries.get(key);
361
- if (existing) {
362
- clearTimeout(existing.timer);
363
- }
364
- const timer = setTimeout(() => {
365
- this.pendingReplaceDeliveries.delete(key);
366
- this.deliverMessage(toId, from, message);
367
- }, REPLACE_DELIVERY_DELAY_MS);
368
- timer.unref();
369
- this.pendingReplaceDeliveries.set(key, { fromId, toId, message, timer });
356
+ this.replacements.stage(fromId, toId, from, message);
370
357
  return {
371
358
  id: message.id,
372
359
  accepted: true,
@@ -375,15 +362,6 @@ class IntercomBroker {
375
362
  reason: "Queued for replace-mode delivery",
376
363
  };
377
364
  }
378
- clearPendingReplaceDeliveries(sessionId) {
379
- for (const [key, pending] of this.pendingReplaceDeliveries) {
380
- if (pending.toId === sessionId ||
381
- (pending.fromId === sessionId && pending.message.expectsReply === true)) {
382
- clearTimeout(pending.timer);
383
- this.pendingReplaceDeliveries.delete(key);
384
- }
385
- }
386
- }
387
365
  /** One admission rule; retained input is checked as the uncompacted envelope any later
388
366
  * transport generation accepts. */
389
367
  validateDeliveryPayload(from, message, target) {
@@ -3,6 +3,7 @@ import { errorMessage } from "../../shared/unknown.js";
3
3
  import { isNativeReadQuery, isNativeReadResult, isLastMessageResult, isReceiptResult, } from "./last.js";
4
4
  import { normalizeSessionInfo, } from "../types.js";
5
5
  import { writeMessage } from "./framing.js";
6
+ import { PendingRequests } from "./pending-requests.js";
6
7
  /** A peer's read of this session or, for a main, of its exited `parked` child. */
7
8
  export function lastRequest(msg) {
8
9
  const from = normalizeSessionInfo(msg.from);
@@ -24,26 +25,20 @@ export class LastMessageRequests {
24
25
  constructor(timeoutMs) {
25
26
  this.timeoutMs = timeoutMs;
26
27
  }
27
- pending = new Map();
28
+ pending = new PendingRequests();
28
29
  fail(error) {
29
30
  this.lastMessages = false;
30
31
  this.receiptReads = false;
31
- for (const pending of this.pending.values()) {
32
- clearTimeout(pending.timer);
33
- pending.reject(error);
34
- }
35
- this.pending.clear();
32
+ this.pending.fail(error);
36
33
  }
37
34
  receive(message) {
38
35
  if (typeof message.requestId !== "string" || !isNativeReadResult(message.result)) {
39
36
  throw new Error("Invalid last-message response");
40
37
  }
41
- const pending = this.pending.get(message.requestId);
38
+ const pending = this.pending.take(message.requestId);
42
39
  if (!pending) {
43
40
  return;
44
41
  }
45
- clearTimeout(pending.timer);
46
- this.pending.delete(message.requestId);
47
42
  pending.resolve(message.result);
48
43
  }
49
44
  async last(socket, to, query) {
@@ -66,19 +61,12 @@ export class LastMessageRequests {
66
61
  }
67
62
  return new Promise((resolve, reject) => {
68
63
  const requestId = randomUUID();
69
- const timer = setTimeout(() => {
70
- this.pending.delete(requestId);
71
- reject(new Error("Last-message read timeout"));
72
- }, this.timeoutMs);
73
- timer.unref();
74
- this.pending.set(requestId, { resolve, reject, timer });
64
+ this.pending.set(requestId, { resolve, reject }, this.timeoutMs, () => new Error("Last-message read timeout"));
75
65
  try {
76
66
  writeMessage(socket, { type: "last_request", requestId, to, query });
77
67
  }
78
68
  catch (error) {
79
- clearTimeout(timer);
80
- this.pending.delete(requestId);
81
- reject(new Error(errorMessage(error)));
69
+ this.pending.take(requestId)?.reject(new Error(errorMessage(error)));
82
70
  }
83
71
  });
84
72
  }
@@ -2,7 +2,7 @@ import type net from "node:net";
2
2
  /** Parking negotiation and its single outstanding broker acknowledgement. */
3
3
  export declare class ParkRequests {
4
4
  supported: boolean;
5
- private acknowledged?;
5
+ private state;
6
6
  settle(parked: boolean): void;
7
7
  /** True once the broker retains later input; false when it cannot or the socket closed. */
8
8
  request(socket: () => net.Socket, on: boolean): Promise<boolean>;
@@ -2,20 +2,36 @@ import { writeMessage } from "./framing.js";
2
2
  /** Parking negotiation and its single outstanding broker acknowledgement. */
3
3
  export class ParkRequests {
4
4
  supported = false;
5
- acknowledged;
5
+ state = { kind: "idle" };
6
6
  settle(parked) {
7
- this.acknowledged?.(parked);
7
+ const state = this.state;
8
+ this.state = { kind: "idle" };
9
+ if (state.kind === "pending") {
10
+ state.resolve(parked);
11
+ }
8
12
  }
9
13
  /** True once the broker retains later input; false when it cannot or the socket closed. */
10
14
  request(socket, on) {
11
- return new Promise((resolve) => {
12
- this.acknowledged = on && this.supported ? resolve : undefined;
15
+ if (on && this.state.kind === "pending") {
16
+ return this.state.promise;
17
+ }
18
+ const { promise, resolve, reject } = Promise.withResolvers();
19
+ this.settle(false);
20
+ if (on && this.supported) {
21
+ this.state = { kind: "pending", promise, resolve };
22
+ }
23
+ try {
13
24
  if (this.supported) {
14
25
  writeMessage(socket(), { type: on ? "park" : "unpark" });
15
26
  }
16
- if (this.acknowledged === undefined) {
27
+ if (this.state.kind === "idle") {
17
28
  resolve(false);
18
29
  }
19
- });
30
+ }
31
+ catch (error) {
32
+ this.state = { kind: "idle" };
33
+ reject(error);
34
+ }
35
+ return promise;
20
36
  }
21
37
  }
@@ -18,8 +18,7 @@ export declare class IntercomClient extends EventEmitter {
18
18
  private readonly lastRequests;
19
19
  private readonly pendingSends;
20
20
  private readonly pendingLists;
21
- private connecting;
22
- private disconnecting;
21
+ private phase;
23
22
  private readonly sendTimeoutMs;
24
23
  private readonly listTimeoutMs;
25
24
  constructor(options?: IntercomClientOptions);
@@ -30,6 +29,7 @@ export declare class IntercomClient extends EventEmitter {
30
29
  isConnected(): boolean;
31
30
  private requireActiveSocket;
32
31
  connect(session: Omit<SessionInfo, "id">, requestedId?: string): Promise<void>;
32
+ private establish;
33
33
  private restoreTopics;
34
34
  private handleBrokerMessage;
35
35
  private receiveRegistration;
@@ -39,6 +39,7 @@ export declare class IntercomClient extends EventEmitter {
39
39
  private receiveSessions;
40
40
  park(on?: boolean): Promise<boolean>;
41
41
  disconnect(): Promise<void>;
42
+ private close;
42
43
  listSessions(): Promise<SessionInfo[]>;
43
44
  last(to: string, query?: LastMessageQuery): Promise<LastMessageResult>;
44
45
  receipt(to: string, query: ReceiptQuery): Promise<ReceiptResult>;