@fitchmultz/pi-subagents 0.52.7 → 0.52.9

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 (80) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/agent/launch.js +1 -1
  3. package/dist/agent/observation-view.js +15 -7
  4. package/dist/agent/observe-card.js +2 -2
  5. package/dist/agent/observe-client.js +1 -1
  6. package/dist/agent/observe.js +4 -1
  7. package/dist/agent/process.js +5 -3
  8. package/dist/agent/runner.js +9 -4
  9. package/dist/agent/saved.js +16 -6
  10. package/dist/agent/view.js +5 -2
  11. package/dist/build-info.js +1 -1
  12. package/dist/pi-intercom/bridge-routes.js +1 -1
  13. package/dist/pi-intercom/bridge.js +4 -3
  14. package/dist/pi-intercom/broker/broker.js +1 -1
  15. package/dist/pi-intercom/broker/client-last.d.ts +1 -1
  16. package/dist/pi-intercom/broker/client.d.ts +9 -9
  17. package/dist/pi-intercom/broker/framing.js +6 -0
  18. package/dist/pi-intercom/broker/last.d.ts +5 -1
  19. package/dist/pi-intercom/broker/last.js +33 -15
  20. package/dist/pi-intercom/broker/parked.js +1 -1
  21. package/dist/pi-intercom/outbound.js +1 -0
  22. package/dist/pi-intercom/reply-tracker.js +11 -5
  23. package/dist/pi-intercom/session-targets.d.ts +2 -2
  24. package/dist/pi-intercom/session-targets.js +3 -2
  25. package/dist/pi-intercom/tool-registration.js +1 -0
  26. package/dist/pi-intercom/types.d.ts +37 -37
  27. package/dist/pi-intercom/types.js +9 -7
  28. package/dist/shared/output-page.js +1 -1
  29. package/package.json +5 -4
  30. package/scripts/archive-retired-history.mjs +3 -1
  31. package/src/agent/index.ts +3 -3
  32. package/src/agent/journal-tail.ts +2 -2
  33. package/src/agent/launch.ts +1 -1
  34. package/src/agent/notifications.ts +3 -3
  35. package/src/agent/observation-state.ts +2 -2
  36. package/src/agent/observation-view.ts +20 -7
  37. package/src/agent/observation.ts +4 -4
  38. package/src/agent/observe-card.ts +7 -3
  39. package/src/agent/observe-chrome.ts +2 -2
  40. package/src/agent/observe-client.ts +5 -5
  41. package/src/agent/observe-reading.ts +1 -1
  42. package/src/agent/observe-screen.ts +4 -4
  43. package/src/agent/observe-transcript.ts +4 -4
  44. package/src/agent/observe.ts +13 -8
  45. package/src/agent/parking.ts +1 -1
  46. package/src/agent/process.ts +6 -4
  47. package/src/agent/runner.ts +15 -7
  48. package/src/agent/saved.ts +17 -7
  49. package/src/agent/usage.ts +1 -1
  50. package/src/agent/view.ts +18 -9
  51. package/src/agent/wake.ts +2 -2
  52. package/src/pi-intercom/bridge-config.ts +2 -2
  53. package/src/pi-intercom/bridge-mailbox.ts +1 -1
  54. package/src/pi-intercom/bridge-presence.ts +1 -1
  55. package/src/pi-intercom/bridge-routes.ts +5 -5
  56. package/src/pi-intercom/bridge-session.ts +5 -5
  57. package/src/pi-intercom/bridge-wake.ts +2 -2
  58. package/src/pi-intercom/bridge.ts +4 -3
  59. package/src/pi-intercom/broker/broker.ts +1 -1
  60. package/src/pi-intercom/broker/client-last.ts +1 -1
  61. package/src/pi-intercom/broker/client-park.ts +1 -1
  62. package/src/pi-intercom/broker/client.ts +9 -9
  63. package/src/pi-intercom/broker/framing.ts +6 -0
  64. package/src/pi-intercom/broker/last.ts +62 -18
  65. package/src/pi-intercom/broker/parked.ts +8 -5
  66. package/src/pi-intercom/config.ts +1 -1
  67. package/src/pi-intercom/outbound.ts +1 -0
  68. package/src/pi-intercom/reply-tracker.ts +13 -6
  69. package/src/pi-intercom/reply-wait.ts +3 -3
  70. package/src/pi-intercom/runtime-types.ts +1 -1
  71. package/src/pi-intercom/session-targets.ts +5 -4
  72. package/src/pi-intercom/tool-registration.ts +1 -0
  73. package/src/pi-intercom/tool-results.ts +1 -1
  74. package/src/pi-intercom/types.ts +48 -43
  75. package/src/shared/journal-frames.ts +1 -1
  76. package/src/shared/json-projection.ts +2 -2
  77. package/src/shared/native-journal.ts +2 -2
  78. package/src/shared/output-page.ts +3 -3
  79. package/src/shared/post-exit-stdio-guard.ts +4 -4
  80. package/tsconfig.json +8 -1
@@ -47,10 +47,20 @@ function ownedRun(context: ExtensionContext, id: string) {
47
47
  throw new Error("Legacy continuation requires a published native owner.");
48
48
  }
49
49
  const journal = new NativeJournal(file, "live");
50
- if (journal.records[0].value.id !== context.sessionManager.getSessionId()) {
50
+ const [opening] = journal.records;
51
+ if (opening === undefined || opening.value.id !== context.sessionManager.getSessionId()) {
51
52
  throw new Error("Native owner journal identity changed; nothing was launched.");
52
53
  }
53
- let owned;
54
+ const owned = findOwnedRun(journal, id);
55
+ if (owned?.ownerSessionId !== context.sessionManager.getSessionId()) {
56
+ throw new Error("Missing saved native owner or foreign legacy run; nothing was launched.");
57
+ }
58
+ Assert(singleSchema, owned);
59
+ return owned;
60
+ }
61
+ /** The most recent saved subagent-run record for this run id, if one exists. */
62
+ function findOwnedRun(journal: NativeJournal, id: string): Record<string, unknown> | undefined {
63
+ let owned: Record<string, unknown> | undefined;
54
64
  for (const record of journal.records) {
55
65
  if (record.value.type === "custom" && record.value.customType === "subagent-run") {
56
66
  const { data } = journal.body(record);
@@ -59,10 +69,6 @@ function ownedRun(context: ExtensionContext, id: string) {
59
69
  }
60
70
  }
61
71
  }
62
- if (owned?.ownerSessionId !== context.sessionManager.getSessionId()) {
63
- throw new Error("Missing saved native owner or foreign legacy run; nothing was launched.");
64
- }
65
- Assert(singleSchema, owned);
66
72
  return owned;
67
73
  }
68
74
  function requireExited(pid: unknown): void {
@@ -101,7 +107,11 @@ function nativeSession(contract: Contract) {
101
107
  throw new Error("Legacy continuation requires the exact saved native journal.");
102
108
  }
103
109
  const journal = new NativeJournal(contract.sessionFile, "live");
104
- const sessionId = journal.body(journal.records[0]).id;
110
+ const [opening] = journal.records;
111
+ if (opening === undefined) {
112
+ throw new Error("Not a readable native Pi session.");
113
+ }
114
+ const sessionId = journal.body(opening).id;
105
115
  if (
106
116
  typeof sessionId !== "string" ||
107
117
  sessionId.length === 0 ||
@@ -74,7 +74,7 @@ function reusable(cached: Prefix | undefined, stat: fs.BigIntStats): Prefix {
74
74
  * accepted LF boundary. Baselines, required usage and attribution apply to each read.
75
75
  */
76
76
  export class UsageIndex {
77
- private prefix?: Prefix;
77
+ private prefix?: Prefix | undefined;
78
78
  read(launch: Launch, baseline: ReadonlySet<string>, terminal?: string): Contribution[] {
79
79
  const values: unknown[] = [];
80
80
  const reachedTerminal = new Error("Native terminal boundary reached.");
package/src/agent/view.ts CHANGED
@@ -36,8 +36,8 @@ interface RunSummary {
36
36
  id: string;
37
37
  owner: string;
38
38
  rootId: string;
39
- predecessor?: string;
40
- name?: string;
39
+ predecessor?: string | undefined;
40
+ name?: string | undefined;
41
41
  cwd: string;
42
42
  model: string;
43
43
  thinking: string;
@@ -45,24 +45,28 @@ interface RunSummary {
45
45
  admission: string;
46
46
  status: string;
47
47
  completed: boolean;
48
- result?: Pick<Result, "status" | "endedAt" | "error">;
48
+ result?: Pick<Result, "status" | "endedAt" | "error"> | undefined;
49
49
  nativeReceipt: string | null;
50
50
  process: ReturnType<typeof processView>;
51
- lastWakeFailure?: { at: number; reason: string };
51
+ lastWakeFailure?: { at: number; reason: string } | undefined;
52
52
  sessionId: string;
53
53
  sessionFile: string;
54
54
  outputFile: string;
55
55
  guidance: Guidance[];
56
56
  }
57
57
  interface RunView extends Omit<RunSummary, "result"> {
58
- result?: Result;
58
+ result?: Result | undefined;
59
59
  output: ReturnType<typeof outputView>;
60
60
  }
61
61
  type Receipt =
62
62
  | { readonly receipt: "launch"; readonly note: string }
63
63
  | { readonly receipt: "continuation" | "existing-successor" }
64
64
  | { readonly receipt: "guidance"; readonly command: string; readonly note: string }
65
- | { readonly stopRequest?: string; readonly stopAcknowledged: false; readonly note: string };
65
+ | {
66
+ readonly stopRequest?: string | undefined;
67
+ readonly stopAcknowledged: false;
68
+ readonly note: string;
69
+ };
66
70
 
67
71
  function toolResult(value: unknown, text: string): ToolReply {
68
72
  return { content: [{ type: "text" as const, text }], details: value };
@@ -144,7 +148,7 @@ function metadata(
144
148
  launch: Launch,
145
149
  result: Result | undefined,
146
150
  options: {
147
- ctx?: ExtensionContext;
151
+ ctx?: ExtensionContext | undefined;
148
152
  guidance: Guidance[];
149
153
  observed?: ReadonlyMap<string, boolean>;
150
154
  },
@@ -197,7 +201,12 @@ async function summaries(
197
201
  ...new Map(launches.map((launch) => [launch.rootId, actor(base, launch)])).values(),
198
202
  ].filter((identity) => identity !== undefined);
199
203
  const verdicts = await observeAlive(identities);
200
- const observed = new Map(identities.map((identity, index) => [identity.token, verdicts[index]]));
204
+ const observed = new Map(
205
+ identities.flatMap((identity, index) => {
206
+ const verdict = verdicts[index];
207
+ return verdict === undefined ? [] : ([[identity.token, verdict]] as const);
208
+ }),
209
+ );
201
210
  return launches.map((launch) => {
202
211
  const view = metadata(base, launch, readCanonicalResult(base, launch), {
203
212
  ctx,
@@ -211,7 +220,7 @@ async function summaries(
211
220
  ? {
212
221
  status: view.result.status,
213
222
  endedAt: view.result.endedAt,
214
- error: view.result.error?.slice(0, 2048),
223
+ ...(view.result.error !== undefined ? { error: view.result.error.slice(0, 2048) } : {}),
215
224
  }
216
225
  : undefined,
217
226
  };
package/src/agent/wake.ts CHANGED
@@ -21,7 +21,7 @@ export type WatchState = "unwatched" | "armed" | "changed" | "watching";
21
21
 
22
22
  /** Native change hints for one directory identity; owners still reconcile canonical files. */
23
23
  export class DirectoryWatch {
24
- private watcher?: fs.FSWatcher;
24
+ private watcher?: fs.FSWatcher | undefined;
25
25
  private armed = "";
26
26
  private checked = "";
27
27
  private failed = false;
@@ -78,7 +78,7 @@ export class DirectoryWatch {
78
78
  /** A coalesced wake for one owner loop; raised until that loop takes it. */
79
79
  export class Wake {
80
80
  private raised = true;
81
- private resolve?: () => void;
81
+ private resolve?: (() => void) | undefined;
82
82
  readonly raise = (): void => {
83
83
  this.raised = true;
84
84
  this.resolve?.();
@@ -39,10 +39,10 @@ export interface Identity {
39
39
  readonly fingerprint256: string;
40
40
  readonly name: string;
41
41
  readonly cwd: string;
42
- readonly wake?: WakePolicy;
42
+ readonly wake?: WakePolicy | undefined;
43
43
  }
44
44
  export interface Config {
45
- readonly stateDir?: string;
45
+ readonly stateDir?: string | undefined;
46
46
  readonly port: number;
47
47
  readonly ca: string;
48
48
  readonly cert: string;
@@ -76,7 +76,7 @@ function decode(value: unknown): Envelope {
76
76
 
77
77
  /** Persistent payloads leave only ID tombstones after acknowledgement, preventing replay resurrection. */
78
78
  export class Mailbox {
79
- private readonly db?: DatabaseSync;
79
+ private readonly db?: DatabaseSync | undefined;
80
80
  private readonly entries = new Map<string, Entry>();
81
81
  private bytes = 0;
82
82
  private sequence = 0;
@@ -22,7 +22,7 @@ export class PersistentPresence {
22
22
  private readonly registrations = new Map<string, Registration>();
23
23
  private stopped = false;
24
24
  private timer?: NodeJS.Timeout;
25
- private running?: Promise<void>;
25
+ private running?: Promise<void> | undefined;
26
26
  private readonly owner: PresenceOwner;
27
27
 
28
28
  constructor(owner: PresenceOwner) {
@@ -16,10 +16,10 @@ export type Operation =
16
16
  readonly route: "POST /v1/send";
17
17
  readonly to: string;
18
18
  readonly message: string;
19
- readonly passive?: boolean;
20
- readonly messageId?: string;
21
- readonly replyTo?: string;
22
- readonly expectsReply?: boolean;
19
+ readonly passive?: boolean | undefined;
20
+ readonly messageId?: string | undefined;
21
+ readonly replyTo?: string | undefined;
22
+ readonly expectsReply?: boolean | undefined;
23
23
  };
24
24
  type Route = Operation["route"];
25
25
  const fields = {
@@ -39,7 +39,7 @@ export function routeFor(req: IncomingMessage): Route {
39
39
  ) {
40
40
  throw new Fault(403, "browser_forbidden", "Browser access is not supported.");
41
41
  }
42
- const route = `${req.method ?? ""} ${(req.url ?? "").split("?")[0]}`;
42
+ const route = `${req.method ?? ""} ${(req.url ?? "").split("?")[0] ?? ""}`;
43
43
  if (!isRoute(route)) {
44
44
  throw new Fault(404, "not_found", "Unknown method or path.");
45
45
  }
@@ -16,12 +16,12 @@ const SEND_TIMEOUT = 8000;
16
16
  const LIST_TIMEOUT = 5000;
17
17
 
18
18
  interface SendOptions extends Pick<Message, "replyTo" | "expectsReply" | "delivery"> {
19
- readonly messageId?: string;
19
+ readonly messageId?: string | undefined;
20
20
  }
21
21
  export interface SessionOwner {
22
- readonly stateDir?: string;
23
- readonly notifications?: boolean;
24
- readonly mailboxChanged?: () => void;
22
+ readonly stateDir?: string | undefined;
23
+ readonly notifications?: boolean | undefined;
24
+ readonly mailboxChanged?: (() => void) | undefined;
25
25
  readonly allowed: (identity: Identity) => boolean;
26
26
  readonly lost: (fp: string, count?: number) => void;
27
27
  readonly retired: (fp: string, generation: string) => void;
@@ -42,7 +42,7 @@ export class Session {
42
42
  private inboxChanged = new AbortController();
43
43
  private closed = false;
44
44
  private connecting?: Promise<void>;
45
- private disconnecting?: Promise<void>;
45
+ private disconnecting?: Promise<void> | undefined;
46
46
  private retirement?: Promise<void>;
47
47
  private lease?: NodeJS.Timeout;
48
48
  private closeReason: Fault = new Fault(503, "broker_unavailable", "Broker connection failed.");
@@ -35,12 +35,12 @@ function bucket(identity: Identity): string {
35
35
  /** Mailbox commits and durable retry/quota deadlines drive wakes; there is no intake scan timer. */
36
36
  export class WakeDeliveries {
37
37
  private readonly stateDir: string | undefined;
38
- private readonly state?: WakeState;
38
+ private readonly state?: WakeState | undefined;
39
39
  private readonly allowed: (identity: Identity) => boolean;
40
40
  private readonly deliveries = new Map<string, Delivery>();
41
41
  private readonly controller = new AbortController();
42
42
  private timer?: NodeJS.Timeout;
43
- private running?: Promise<void>;
43
+ private running?: Promise<void> | undefined;
44
44
  private dirty = false;
45
45
  private storageBackoff = 15000;
46
46
 
@@ -417,19 +417,20 @@ class Bridge {
417
417
 
418
418
  function main(): void {
419
419
  const args = process.argv.slice(2);
420
- if (args.length === 1 && (args[0] === "-h" || args[0] === "--help")) {
420
+ const [flag, configPath] = args;
421
+ if (args.length === 1 && (flag === "-h" || flag === "--help")) {
421
422
  process.stdout.write(
422
423
  "Usage: node dist/pi-intercom/bridge.js --config PATH\n\nOptional loopback HTTPS/mTLS bridge for the local pi-intercom broker.\nConfig and TLS key must be owned by you with mode 0600.\nWithout stateDir, messaging never starts the broker. Durable presence safely starts\none only when none is running; it never restarts or replaces a live broker.\nSIGHUP reloads the client allowlist (invalid reload denies all clients).\nRestart this helper for TLS/listener rotation. SIGINT/SIGTERM disconnect peers.\n\nExample: node dist/pi-intercom/bridge.js --config ~/.pi/agent/intercom/bridge.json\n",
423
424
  );
424
425
  return;
425
426
  }
426
- if (args.length !== 2 || args[0] !== "--config" || args[1] === "") {
427
+ if (args.length !== 2 || flag !== "--config" || configPath === undefined || configPath === "") {
427
428
  process.stderr.write("Usage: node dist/pi-intercom/bridge.js --config PATH (or --help)\n");
428
429
  process.exitCode = 2;
429
430
  return;
430
431
  }
431
432
  try {
432
- const path = resolve(args[1]);
433
+ const path = resolve(configPath);
433
434
  new Bridge(loadConfig(path), path).start();
434
435
  } catch (error) {
435
436
  if (error instanceof Fault) {
@@ -303,7 +303,7 @@ class IntercomBroker {
303
303
  }
304
304
  const targets = findSessions(this.sessions, msg.to, currentId);
305
305
  const target = targets[0];
306
- if (targets.length === 1) {
306
+ if (targets.length === 1 && target !== undefined) {
307
307
  const guardError = guardedDeliveryError(this.sessions, msg.to, message);
308
308
  if (guardError !== null) {
309
309
  writeMessage(socket, {
@@ -24,7 +24,7 @@ export function lastRequest(msg: UnknownRecord): {
24
24
  readonly requestId: string;
25
25
  readonly from: SessionInfo;
26
26
  readonly query: NativeReadQuery;
27
- readonly parked?: SessionInfo;
27
+ readonly parked?: SessionInfo | undefined;
28
28
  } {
29
29
  const from = normalizeSessionInfo(msg.from);
30
30
  if (typeof msg.requestId !== "string" || !from || !isNativeReadQuery(msg.query)) {
@@ -4,7 +4,7 @@ import { writeMessage } from "./framing.ts";
4
4
  /** Parking negotiation and its single outstanding broker acknowledgement. */
5
5
  export class ParkRequests {
6
6
  supported = false;
7
- private acknowledged?: (parked: boolean) => void;
7
+ private acknowledged?: ((parked: boolean) => void) | undefined;
8
8
  settle(parked: boolean): void {
9
9
  this.acknowledged?.(parked);
10
10
  }
@@ -516,15 +516,15 @@ export class IntercomClient extends EventEmitter {
516
516
  }
517
517
 
518
518
  updatePresence(updates: {
519
- readonly applicationGuard?: string | null;
520
- readonly name?: string;
521
- readonly status?: string;
522
- readonly model?: string;
523
- readonly pendingAsks?: number;
524
- readonly acceptsAsks?: boolean;
525
- readonly lastIntercomActivity?: number;
526
- readonly subscriptions?: SessionInfo["subscriptions"];
527
- readonly topics?: readonly TopicUpdate[];
519
+ readonly applicationGuard?: string | null | undefined;
520
+ readonly name?: string | undefined;
521
+ readonly status?: string | undefined;
522
+ readonly model?: string | undefined;
523
+ readonly pendingAsks?: number | undefined;
524
+ readonly acceptsAsks?: boolean | undefined;
525
+ readonly lastIntercomActivity?: number | undefined;
526
+ readonly subscriptions?: SessionInfo["subscriptions"] | undefined;
527
+ readonly topics?: readonly TopicUpdate[] | undefined;
528
528
  }): void {
529
529
  if (this.disconnecting) {
530
530
  return;
@@ -58,10 +58,16 @@ class MessageBuffer {
58
58
  if (size === 0) {
59
59
  return Buffer.alloc(0);
60
60
  }
61
+ if (first === undefined) {
62
+ throw new Error("Frame reader has no buffered chunk for a non-empty read.");
63
+ }
61
64
  const result = first.length >= size ? first.subarray(0, size) : Buffer.allocUnsafe(size);
62
65
  let written = 0;
63
66
  while (written < size) {
64
67
  const chunk = this.chunks[this.index];
68
+ if (chunk === undefined) {
69
+ throw new Error("Frame reader ran out of buffered chunks mid-read.");
70
+ }
65
71
  const count = Math.min(chunk.length, size - written);
66
72
  if (first.length < size) {
67
73
  chunk.copy(result, written, 0, count);
@@ -100,13 +100,13 @@ interface LastPeer {
100
100
  readonly info: SessionInfo;
101
101
  readonly lastMessages: boolean;
102
102
  readonly receiptReads: boolean;
103
- readonly parked?: { readonly closed: boolean };
103
+ readonly parked?: { readonly closed: boolean } | undefined;
104
104
  }
105
105
  interface PendingLast {
106
106
  readonly fromId: string;
107
107
  readonly toId: string;
108
108
  /** A parked child's main answers from the child's captured native history. */
109
- readonly via?: string;
109
+ readonly via?: string | undefined;
110
110
  readonly requestId: string;
111
111
  readonly query: NativeReadQuery;
112
112
  readonly timer: NodeJS.Timeout;
@@ -130,24 +130,25 @@ function unavailable(
130
130
  targets: readonly LastPeer[],
131
131
  name: string,
132
132
  pending: number,
133
- { query, via }: { readonly query: NativeReadQuery; readonly via?: LastPeer },
133
+ { query, via }: { readonly query: NativeReadQuery; readonly via?: LastPeer | undefined },
134
134
  ): string | undefined {
135
- if (targets.length === 0) {
135
+ const [target] = targets;
136
+ if (target === undefined) {
136
137
  return "Session not found";
137
138
  }
138
139
  if (targets.length > 1) {
139
140
  return `Multiple sessions named "${name}" are connected. Use the session ID instead.`;
140
141
  }
141
- const exited = exitedRefusal(targets[0], query, via);
142
+ const exited = exitedRefusal(target, query, via);
142
143
  if (exited !== undefined) {
143
144
  return exited;
144
145
  }
145
- if (!targets[0].lastMessages) {
146
+ if (!target.lastMessages) {
146
147
  return "Recipient does not support last-message reads; reload its extension.";
147
148
  }
148
149
  if (
149
150
  "messageId" in query &&
150
- (!targets[0].receiptReads || targets[0].info.rootSessionId !== query.rootSessionId)
151
+ (!target.receiptReads || target.info.rootSessionId !== query.rootSessionId)
151
152
  ) {
152
153
  return "publication_unavailable";
153
154
  }
@@ -213,19 +214,19 @@ export class LastMessageRelay {
213
214
  ): void {
214
215
  const message = lastRequest(input);
215
216
  const targets = findSessions(this.sessions, message.to, fromId);
216
- const target = targets[0];
217
217
  const from = this.sessions.get(fromId);
218
218
  if (!from) {
219
219
  return;
220
220
  }
221
221
  const token = randomUUID();
222
- const via = targets.length === 1 ? reader(target.info.id) : undefined;
222
+ const delegated = this.delegatedReader(targets, reader);
223
+ const via = delegated?.via;
223
224
  const frame = {
224
225
  type: "last_request" as const,
225
226
  requestId: token,
226
227
  from: messageSender(from.info),
227
228
  query: message.query,
228
- parked: via && messageSender(target.info),
229
+ parked: delegated === undefined ? undefined : messageSender(delegated.target.info),
229
230
  };
230
231
  const error =
231
232
  unavailable(targets, message.to, this.pending.size, { query: message.query, via }) ??
@@ -238,24 +239,67 @@ export class LastMessageRelay {
238
239
  });
239
240
  return;
240
241
  }
242
+ const [recipient] = targets;
243
+ if (recipient === undefined) {
244
+ return;
245
+ }
246
+ this.forward(
247
+ { token, fromId, requestId: message.requestId, query: message.query },
248
+ recipient,
249
+ via,
250
+ frame,
251
+ );
252
+ }
253
+
254
+ /** Register the correlated wait, then forward the read to the reader or the recipient. */
255
+ private forward(
256
+ wait: {
257
+ readonly token: string;
258
+ readonly fromId: string;
259
+ readonly requestId: string;
260
+ readonly query: NativeReadQuery;
261
+ },
262
+ recipient: LastPeer,
263
+ via: LastPeer | undefined,
264
+ frame: {
265
+ readonly type: "last_request";
266
+ readonly requestId: string;
267
+ readonly from: SessionInfo;
268
+ readonly query: NativeReadQuery;
269
+ readonly parked?: SessionInfo | undefined;
270
+ },
271
+ ): void {
241
272
  const timer = setTimeout(
242
- () => this.finish(token, { error: "Last-message read timed out" }),
273
+ () => this.finish(wait.token, { error: "Last-message read timed out" }),
243
274
  5000,
244
275
  );
245
276
  timer.unref();
246
- this.pending.set(token, {
247
- fromId,
248
- toId: target.info.id,
277
+ this.pending.set(wait.token, {
278
+ fromId: wait.fromId,
279
+ toId: recipient.info.id,
249
280
  via: via?.info.id,
250
- requestId: message.requestId,
251
- query: message.query,
281
+ requestId: wait.requestId,
282
+ query: wait.query,
252
283
  timer,
253
284
  });
254
285
  try {
255
- writeMessage((via ?? target).socket, frame);
286
+ writeMessage((via ?? recipient).socket, frame);
256
287
  } catch {
257
- this.finish(token, { error: "Recipient disconnected before last-message read" });
288
+ this.finish(wait.token, { error: "Recipient disconnected before last-message read" });
289
+ }
290
+ }
291
+
292
+ /** A sole matched target may delegate the read to another peer's captured history. */
293
+ private delegatedReader(
294
+ targets: readonly LastPeer[],
295
+ reader: (id: string) => LastPeer | undefined,
296
+ ): { readonly target: LastPeer; readonly via: LastPeer } | undefined {
297
+ const [target] = targets;
298
+ if (targets.length !== 1 || target === undefined) {
299
+ return undefined;
258
300
  }
301
+ const via = reader(target.info.id);
302
+ return via === undefined ? undefined : { target, via };
259
303
  }
260
304
 
261
305
  respond(fromId: string, message: UnknownRecord): void {
@@ -21,8 +21,8 @@ interface Parked {
21
21
  readonly retained: Retained;
22
22
  /** The fenced actor's socket closed: input waits for its restored replacement. */
23
23
  closed: boolean;
24
- attempt?: WakeAttempt;
25
- lastFailure?: string;
24
+ attempt?: WakeAttempt | undefined;
25
+ lastFailure?: string | undefined;
26
26
  }
27
27
  export interface ParkablePeer {
28
28
  readonly socket: net.Socket;
@@ -31,7 +31,7 @@ export interface ParkablePeer {
31
31
  readonly parking: boolean;
32
32
  readonly wakeResults: boolean;
33
33
  readonly wakeNotices: boolean;
34
- parked?: Parked;
34
+ parked?: Parked | undefined;
35
35
  }
36
36
  export function reachable<T extends ParkablePeer>(target?: T): target is T {
37
37
  const socket = target?.socket;
@@ -101,7 +101,10 @@ export class ParkedSessions<T extends ParkablePeer> {
101
101
  }
102
102
  }
103
103
  /** A free requested identity, or a parked one resumed by the same child lineage. */
104
- claim(requestedId: unknown, next: Omit<SessionInfo, "id">): { id: string; resumed?: T } {
104
+ claim(
105
+ requestedId: unknown,
106
+ next: Omit<SessionInfo, "id">,
107
+ ): { id: string; resumed?: T | undefined } {
105
108
  const id = requested(requestedId);
106
109
  const previous = id === undefined ? undefined : this.sessions.get(id);
107
110
  const resumes =
@@ -112,7 +115,7 @@ export class ParkedSessions<T extends ParkablePeer> {
112
115
  return { id: randomUUID() };
113
116
  }
114
117
  previous?.socket.destroy();
115
- return { id, resumed: previous };
118
+ return { id, ...(previous !== undefined ? { resumed: previous } : {}) };
116
119
  }
117
120
  /** Flushes a restored child's input and re-sends wakes owed to a (re)registered main. */
118
121
  registered(id: string, previous?: T): void {
@@ -12,7 +12,7 @@ export interface IntercomConfig {
12
12
  /** Require confirmation before non-reply sends from interactive sessions. */
13
13
  readonly confirmSend: boolean;
14
14
  /** Optional custom status suffix shown after automatic lifecycle status. */
15
- readonly status?: string;
15
+ readonly status?: string | undefined;
16
16
  /** Show reply hint in incoming messages (default: true). */
17
17
  readonly replyHint: boolean;
18
18
  /** Ordinary ask reply timeout, in milliseconds (default: 120000 = 2 minutes). */
@@ -93,6 +93,7 @@ export class IntercomOutbound {
93
93
  case "queue":
94
94
  return " (intentionally deferred behind active recipient work)";
95
95
  case undefined:
96
+ default:
96
97
  return " (defaults to steer; wakes idle recipients and steers active recipients after the current tool call)";
97
98
  }
98
99
  }
@@ -79,8 +79,9 @@ function explicitReply(
79
79
  }
80
80
  function replyBySender(contexts: readonly IntercomContext[], to: string): IntercomContext {
81
81
  const matches = matchingSender(contexts, to);
82
- if (matches.length === 1) {
83
- return matches[0];
82
+ const [onlyMatch] = matches;
83
+ if (matches.length === 1 && onlyMatch !== undefined) {
84
+ return onlyMatch;
84
85
  }
85
86
  if (matches.length > 1) {
86
87
  throw new RecoveryError(
@@ -164,7 +165,7 @@ export class ReplyTracker {
164
165
  const beforeQueued = this.pendingTurnContexts.length;
165
166
  for (let index = this.pendingTurnContexts.length - 1; index >= 0; index -= 1) {
166
167
  const context = this.pendingTurnContexts[index];
167
- if (expires(context)) {
168
+ if (context !== undefined && expires(context)) {
168
169
  this.pendingTurnContexts.splice(index, 1);
169
170
  }
170
171
  }
@@ -181,7 +182,7 @@ export class ReplyTracker {
181
182
  }
182
183
 
183
184
  resolveReplyTarget(
184
- options: { readonly to?: string; readonly replyTo?: string },
185
+ options: { readonly to?: string | undefined; readonly replyTo?: string | undefined },
185
186
  now = Date.now(),
186
187
  ): IntercomContext {
187
188
  this.pruneExpired(now);
@@ -204,11 +205,17 @@ export class ReplyTracker {
204
205
  if (to !== "") {
205
206
  return replyBySender(contexts, to);
206
207
  }
208
+ return this.implicitReplyTarget(pending);
209
+ }
210
+
211
+ /** Without an explicit target, the turn context wins, then a sole pending ask. */
212
+ private implicitReplyTarget(pending: readonly IntercomContext[]): IntercomContext {
207
213
  if (this.currentTurnContext) {
208
214
  return this.currentTurnContext;
209
215
  }
210
- if (pending.length === 1) {
211
- return pending[0];
216
+ const [solePending] = pending;
217
+ if (pending.length === 1 && solePending !== undefined) {
218
+ return solePending;
212
219
  }
213
220
  if (pending.length === 0) {
214
221
  throw new RecoveryError("no_pending_reply", "No active intercom context to reply to");
@@ -15,13 +15,13 @@ interface ReplyWaiter {
15
15
  export interface ReplyWaitOptions {
16
16
  readonly from: string;
17
17
  readonly replyTo: string;
18
- readonly signal?: AbortSignal;
18
+ readonly signal?: AbortSignal | undefined;
19
19
  }
20
20
  export interface AskTransaction {
21
21
  readonly to: string;
22
22
  readonly questionId: string;
23
23
  readonly options: Parameters<IntercomClient["send"]>[1];
24
- readonly signal?: AbortSignal;
24
+ readonly signal?: AbortSignal | undefined;
25
25
  readonly onSent: (result: SendResult) => void;
26
26
  }
27
27
  /** One correlated blocking wait; admitted before send so a fast reply cannot race it. */
@@ -53,7 +53,7 @@ export class ReplyWait {
53
53
  }
54
54
  }
55
55
  receive(
56
- sender: { readonly id: string; readonly name?: string },
56
+ sender: { readonly id: string; readonly name?: string | undefined },
57
57
  message: Message,
58
58
  onMatch: () => void,
59
59
  ): boolean {
@@ -3,7 +3,7 @@ import type { Attachment, Message, MessageDelivery, QueueMode, SessionInfo } fro
3
3
  export interface InboundMessageEntry {
4
4
  readonly from: SessionInfo;
5
5
  readonly message: Message;
6
- readonly replyCommand?: string;
6
+ readonly replyCommand?: string | undefined;
7
7
  readonly bodyText: string;
8
8
  }
9
9
  export interface PendingInboundMessage extends InboundMessageEntry {