@fitchmultz/pi-subagents 0.52.7 → 0.52.8

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 +15 -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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## 0.52.8 - 2026-11-02
6
+
7
+ <!-- npm-release:unreleased:start -->
8
+ <!-- npm-release:unreleased:end -->
9
+
10
+ - Merge pull request #321 from fitchmultz/chore/oxlint-v512-strict-ts ([600fcaa](https://github.com/fitchmultz/pi-subagents/commit/600fcaa9b22d59d4111179a8563332ff0420f9d0))
11
+
12
+ ### What's Changed
13
+
14
+ - Align Oxlint toolchain and baseline config with the strict standard (1/5) by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/319
15
+ - Narrow the node:test registration allowance to package-origin distinct names (2/5) by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/320
16
+ - Adopt the strict compiler options and clear every strict-mode error (3/5) by @fitchmultz in https://github.com/fitchmultz/pi-subagents/pull/321
17
+
18
+ **Full Changelog**: https://github.com/fitchmultz/pi-subagents/compare/v0.52.7...v0.52.8
19
+
5
20
  ## 0.52.7 - 2026-11-01
6
21
 
7
22
  <!-- npm-release:unreleased:start -->
@@ -89,7 +89,7 @@ export async function prepareLaunch(options) {
89
89
  model: selection.model.id,
90
90
  thinking: selection.thinking,
91
91
  trusted,
92
- name: input.name,
92
+ ...(input.name !== undefined ? { name: input.name } : {}),
93
93
  createdAt: Date.now(),
94
94
  };
95
95
  // Retain even unpublished native journals on failure, including full fork evidence.
@@ -197,7 +197,7 @@ export class Viewer {
197
197
  }
198
198
  message(frame, state) {
199
199
  const live = state.message;
200
- if (live === undefined) {
200
+ if (live === undefined || live.message === undefined) {
201
201
  return undefined;
202
202
  }
203
203
  const reset = this.key !== live.key;
@@ -213,18 +213,26 @@ export class Viewer {
213
213
  this.ended = live.ended;
214
214
  const limit = Math.min(content.length, BLOCKS);
215
215
  this.blocks.length = Math.min(this.blocks.length, limit);
216
- for (let index = 0; index < limit && !frame.full; index++) {
217
- const item = this.cursor(index, content[index]).next(index, frame, state);
218
- if (item !== undefined) {
219
- frame.blocks.push(item);
220
- }
221
- }
216
+ this.appendBlocks(frame, state, content, limit);
222
217
  if (!changed && frame.blocks.length === 0) {
223
218
  return undefined;
224
219
  }
225
220
  // An ended message stays live until its session entry is found and handed over.
226
221
  return `{"key":${live.key},"reset":${reset},"ended":${live.ended},"count":${content.length},"blocks":[${frame.blocks.join(",")}]}`;
227
222
  }
223
+ /** Refill the frame from live content blocks, reusing cursors by block identity. */
224
+ appendBlocks(frame, state, content, limit) {
225
+ for (let index = 0; index < limit && !frame.full; index++) {
226
+ const block = content[index];
227
+ if (block === undefined) {
228
+ continue;
229
+ }
230
+ const item = this.cursor(index, block).next(index, frame, state);
231
+ if (item !== undefined) {
232
+ frame.blocks.push(item);
233
+ }
234
+ }
235
+ }
228
236
  cursor(index, block) {
229
237
  const existing = this.blocks.at(index);
230
238
  if (existing?.ref === block) {
@@ -119,7 +119,7 @@ export class Card {
119
119
  this.tool.updateResult({
120
120
  content: blocks(message.content)
121
121
  .filter((part) => part.type === "text")
122
- .map((part) => ({ type: "text", text: part.text })),
122
+ .map((part) => part.text === undefined ? { type: "text" } : { type: "text", text: part.text }),
123
123
  isError: message.isError === true,
124
124
  });
125
125
  this.tool.setExpanded(this.expanded);
@@ -169,7 +169,7 @@ export class Card {
169
169
  this.tool?.updateResult({
170
170
  content: blocks(result.content)
171
171
  .filter((part) => part.type === "text")
172
- .map((part) => ({ type: "text", text: part.text })),
172
+ .map((part) => part.text === undefined ? { type: "text" } : { type: "text", text: part.text }),
173
173
  isError,
174
174
  }, partial);
175
175
  }
@@ -259,7 +259,7 @@ export class ObservationClient {
259
259
  }
260
260
  /** Retire live state only after its exact journal entries have been indexed. */
261
261
  settle(has) {
262
- while (this.handovers.length > 0 && has(this.handovers[0][1])) {
262
+ while (this.handovers.length > 0 && has(this.handovers[0]?.[1] ?? "")) {
263
263
  const item = this.handovers.shift();
264
264
  if (item) {
265
265
  this.retire(item[2], item[3]);
@@ -199,7 +199,10 @@ class Observer {
199
199
  if (this.ctx !== ctx) {
200
200
  return;
201
201
  }
202
- const live = new Map(claims.map(({ root }, index) => [root, verdicts[index]]));
202
+ const live = new Map(claims.flatMap(({ root }, index) => {
203
+ const verdict = verdicts[index];
204
+ return verdict === undefined ? [] : [[root, verdict]];
205
+ }));
203
206
  for (const commit of commits) {
204
207
  commit(live);
205
208
  }
@@ -70,7 +70,7 @@ export class ParentCheck {
70
70
  try {
71
71
  const [live] = await observeAlive([attachment]);
72
72
  if (this.current === check &&
73
- !live &&
73
+ live !== true &&
74
74
  read(file, attachmentSchema)?.generation === attachment.generation) {
75
75
  detached();
76
76
  }
@@ -94,10 +94,11 @@ function replaced(identity) {
94
94
  }
95
95
  export function ownIdentity(pid = process.pid) {
96
96
  // An ordinary main or managed launcher need not be a group leader. Ambiguous live PIDs fail closed.
97
+ const clock = readProcessClock();
97
98
  return {
98
99
  pid,
99
100
  identity: readChildProcessIdentity(pid) ?? "unverified",
100
- clock: readProcessClock(),
101
+ ...(clock !== undefined ? { clock } : {}),
101
102
  };
102
103
  }
103
104
  export function actor(base, launch) {
@@ -177,11 +178,12 @@ async function spawnActor(base, launch, token, claim) {
177
178
  trySignalChildTree(child, "SIGKILL");
178
179
  });
179
180
  child.unref();
181
+ const clock = readProcessClock();
180
182
  write(path.join(dir, "actor.lock", "process.json"), {
181
183
  pid: child.pid,
182
184
  identity: readChildProcessIdentity(child.pid) ?? "unverified",
183
185
  token,
184
- clock: readProcessClock(),
186
+ ...(clock !== undefined ? { clock } : {}),
185
187
  });
186
188
  }
187
189
  async function awaitStopping(dir, existing, signal) {
@@ -31,6 +31,10 @@ function completionStatus(stopping, message, error) {
31
31
  }
32
32
  return "completed";
33
33
  }
34
+ /** The recorded failure: the launch error, the model's error, or the missing terminal message. */
35
+ const failureReason = (error, message) => error ??
36
+ message?.errorMessage ??
37
+ (message ? undefined : "Native run produced no terminal assistant message.");
34
38
  class Actor {
35
39
  active;
36
40
  stopping = false;
@@ -81,16 +85,17 @@ class Actor {
81
85
  const message = terminal?.message;
82
86
  const accounting = this.usage(launch, baseline, terminal?.id);
83
87
  const output = this.output(message);
88
+ const failure = failureReason(error, message);
84
89
  const result = {
85
90
  id: launch.id,
86
91
  owner: launch.owner,
87
92
  status: completionStatus(this.stopping, message, error),
88
93
  endedAt: Date.now(),
89
- terminalEntryId: accounting.accountingError === undefined ? terminal?.id : undefined,
94
+ ...(accounting.accountingError === undefined && terminal?.id !== undefined
95
+ ? { terminalEntryId: terminal.id }
96
+ : {}),
90
97
  ...accounting,
91
- error: error ??
92
- message?.errorMessage ??
93
- (message ? undefined : "Native run produced no terminal assistant message."),
98
+ ...(failure !== undefined ? { error: failure } : {}),
94
99
  };
95
100
  fs.writeFileSync(path.join(runDirectory(this.base, launch.id), "output.txt"), output, {
96
101
  mode: 0o600,
@@ -42,9 +42,19 @@ function ownedRun(context, id) {
42
42
  throw new Error("Legacy continuation requires a published native owner.");
43
43
  }
44
44
  const journal = new NativeJournal(file, "live");
45
- if (journal.records[0].value.id !== context.sessionManager.getSessionId()) {
45
+ const [opening] = journal.records;
46
+ if (opening === undefined || opening.value.id !== context.sessionManager.getSessionId()) {
46
47
  throw new Error("Native owner journal identity changed; nothing was launched.");
47
48
  }
49
+ const owned = findOwnedRun(journal, id);
50
+ if (owned?.ownerSessionId !== context.sessionManager.getSessionId()) {
51
+ throw new Error("Missing saved native owner or foreign legacy run; nothing was launched.");
52
+ }
53
+ Assert(singleSchema, owned);
54
+ return owned;
55
+ }
56
+ /** The most recent saved subagent-run record for this run id, if one exists. */
57
+ function findOwnedRun(journal, id) {
48
58
  let owned;
49
59
  for (const record of journal.records) {
50
60
  if (record.value.type === "custom" && record.value.customType === "subagent-run") {
@@ -54,10 +64,6 @@ function ownedRun(context, id) {
54
64
  }
55
65
  }
56
66
  }
57
- if (owned?.ownerSessionId !== context.sessionManager.getSessionId()) {
58
- throw new Error("Missing saved native owner or foreign legacy run; nothing was launched.");
59
- }
60
- Assert(singleSchema, owned);
61
67
  return owned;
62
68
  }
63
69
  function requireExited(pid) {
@@ -95,7 +101,11 @@ function nativeSession(contract) {
95
101
  throw new Error("Legacy continuation requires the exact saved native journal.");
96
102
  }
97
103
  const journal = new NativeJournal(contract.sessionFile, "live");
98
- const sessionId = journal.body(journal.records[0]).id;
104
+ const [opening] = journal.records;
105
+ if (opening === undefined) {
106
+ throw new Error("Not a readable native Pi session.");
107
+ }
108
+ const sessionId = journal.body(opening).id;
99
109
  if (typeof sessionId !== "string" ||
100
110
  sessionId.length === 0 ||
101
111
  (contract.nativeSessionId !== undefined && contract.nativeSessionId !== sessionId)) {
@@ -111,7 +111,10 @@ async function summaries(base, launches, ctx) {
111
111
  ...new Map(launches.map((launch) => [launch.rootId, actor(base, launch)])).values(),
112
112
  ].filter((identity) => identity !== undefined);
113
113
  const verdicts = await observeAlive(identities);
114
- const observed = new Map(identities.map((identity, index) => [identity.token, verdicts[index]]));
114
+ const observed = new Map(identities.flatMap((identity, index) => {
115
+ const verdict = verdicts[index];
116
+ return verdict === undefined ? [] : [[identity.token, verdict]];
117
+ }));
115
118
  return launches.map((launch) => {
116
119
  const view = metadata(base, launch, readCanonicalResult(base, launch), {
117
120
  ctx,
@@ -125,7 +128,7 @@ async function summaries(base, launches, ctx) {
125
128
  ? {
126
129
  status: view.result.status,
127
130
  endedAt: view.result.endedAt,
128
- error: view.result.error?.slice(0, 2048),
131
+ ...(view.result.error !== undefined ? { error: view.result.error.slice(0, 2048) } : {}),
129
132
  }
130
133
  : undefined,
131
134
  };
@@ -1 +1 @@
1
- export const EXTENSION_BUILD = Object.freeze({"version":"0.52.7","sha256":"ccb2b2cb4725bfa7f5e813494a2007dc60ed9c04e677502d08df23612cc30af8"});
1
+ export const EXTENSION_BUILD = Object.freeze({"version":"0.52.8","sha256":"afeb6d4b7e13dfe796c199d39eddf3836e8f7ee7185f302254f4182659a0388c"});
@@ -16,7 +16,7 @@ export function routeFor(req) {
16
16
  Object.keys(req.headers).some((key) => key.startsWith("sec-fetch-"))) {
17
17
  throw new Fault(403, "browser_forbidden", "Browser access is not supported.");
18
18
  }
19
- const route = `${req.method ?? ""} ${(req.url ?? "").split("?")[0]}`;
19
+ const route = `${req.method ?? ""} ${(req.url ?? "").split("?")[0] ?? ""}`;
20
20
  if (!isRoute(route)) {
21
21
  throw new Fault(404, "not_found", "Unknown method or path.");
22
22
  }
@@ -366,17 +366,18 @@ class Bridge {
366
366
  }
367
367
  function main() {
368
368
  const args = process.argv.slice(2);
369
- if (args.length === 1 && (args[0] === "-h" || args[0] === "--help")) {
369
+ const [flag, configPath] = args;
370
+ if (args.length === 1 && (flag === "-h" || flag === "--help")) {
370
371
  process.stdout.write("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");
371
372
  return;
372
373
  }
373
- if (args.length !== 2 || args[0] !== "--config" || args[1] === "") {
374
+ if (args.length !== 2 || flag !== "--config" || configPath === undefined || configPath === "") {
374
375
  process.stderr.write("Usage: node dist/pi-intercom/bridge.js --config PATH (or --help)\n");
375
376
  process.exitCode = 2;
376
377
  return;
377
378
  }
378
379
  try {
379
- const path = resolve(args[1]);
380
+ const path = resolve(configPath);
380
381
  new Bridge(loadConfig(path), path).start();
381
382
  }
382
383
  catch (error) {
@@ -250,7 +250,7 @@ class IntercomBroker {
250
250
  }
251
251
  const targets = findSessions(this.sessions, msg.to, currentId);
252
252
  const target = targets[0];
253
- if (targets.length === 1) {
253
+ if (targets.length === 1 && target !== undefined) {
254
254
  const guardError = guardedDeliveryError(this.sessions, msg.to, message);
255
255
  if (guardError !== null) {
256
256
  writeMessage(socket, {
@@ -7,7 +7,7 @@ export declare function lastRequest(msg: UnknownRecord): {
7
7
  readonly requestId: string;
8
8
  readonly from: SessionInfo;
9
9
  readonly query: NativeReadQuery;
10
- readonly parked?: SessionInfo;
10
+ readonly parked?: SessionInfo | undefined;
11
11
  };
12
12
  export declare class LastMessageRequests {
13
13
  /** Negotiated at registration; a failed connection supports neither. */
@@ -48,14 +48,14 @@ export declare class IntercomClient extends EventEmitter {
48
48
  private requireSendSocket;
49
49
  send(to: string, options: SendOptions): Promise<SendResult>;
50
50
  updatePresence(updates: {
51
- readonly applicationGuard?: string | null;
52
- readonly name?: string;
53
- readonly status?: string;
54
- readonly model?: string;
55
- readonly pendingAsks?: number;
56
- readonly acceptsAsks?: boolean;
57
- readonly lastIntercomActivity?: number;
58
- readonly subscriptions?: SessionInfo["subscriptions"];
59
- readonly topics?: readonly TopicUpdate[];
51
+ readonly applicationGuard?: string | null | undefined;
52
+ readonly name?: string | undefined;
53
+ readonly status?: string | undefined;
54
+ readonly model?: string | undefined;
55
+ readonly pendingAsks?: number | undefined;
56
+ readonly acceptsAsks?: boolean | undefined;
57
+ readonly lastIntercomActivity?: number | undefined;
58
+ readonly subscriptions?: SessionInfo["subscriptions"] | undefined;
59
+ readonly topics?: readonly TopicUpdate[] | undefined;
60
60
  }): void;
61
61
  }
@@ -48,10 +48,16 @@ class MessageBuffer {
48
48
  if (size === 0) {
49
49
  return Buffer.alloc(0);
50
50
  }
51
+ if (first === undefined) {
52
+ throw new Error("Frame reader has no buffered chunk for a non-empty read.");
53
+ }
51
54
  const result = first.length >= size ? first.subarray(0, size) : Buffer.allocUnsafe(size);
52
55
  let written = 0;
53
56
  while (written < size) {
54
57
  const chunk = this.chunks[this.index];
58
+ if (chunk === undefined) {
59
+ throw new Error("Frame reader ran out of buffered chunks mid-read.");
60
+ }
55
61
  const count = Math.min(chunk.length, size - written);
56
62
  if (first.length < size) {
57
63
  chunk.copy(result, written, 0, count);
@@ -14,7 +14,7 @@ interface LastPeer {
14
14
  readonly receiptReads: boolean;
15
15
  readonly parked?: {
16
16
  readonly closed: boolean;
17
- };
17
+ } | undefined;
18
18
  }
19
19
  /** Correlation belongs to the broker, not the requester or the recipient's model queue. */
20
20
  export declare class LastMessageRelay {
@@ -23,6 +23,10 @@ export declare class LastMessageRelay {
23
23
  constructor(sessions: ReadonlyMap<string, LastPeer>);
24
24
  /** `reader` names another peer that answers for an exited target from its captured history. */
25
25
  request(fromId: string, input: UnknownRecord, reader: (id: string) => LastPeer | undefined): void;
26
+ /** Register the correlated wait, then forward the read to the reader or the recipient. */
27
+ private forward;
28
+ /** A sole matched target may delegate the read to another peer's captured history. */
29
+ private delegatedReader;
26
30
  respond(fromId: string, message: UnknownRecord): void;
27
31
  disconnected(sessionId: string): void;
28
32
  private finish;
@@ -75,21 +75,22 @@ function exitedRefusal(target, query, via) {
75
75
  return via === undefined ? "Parked session history is unavailable." : undefined;
76
76
  }
77
77
  function unavailable(targets, name, pending, { query, via }) {
78
- if (targets.length === 0) {
78
+ const [target] = targets;
79
+ if (target === undefined) {
79
80
  return "Session not found";
80
81
  }
81
82
  if (targets.length > 1) {
82
83
  return `Multiple sessions named "${name}" are connected. Use the session ID instead.`;
83
84
  }
84
- const exited = exitedRefusal(targets[0], query, via);
85
+ const exited = exitedRefusal(target, query, via);
85
86
  if (exited !== undefined) {
86
87
  return exited;
87
88
  }
88
- if (!targets[0].lastMessages) {
89
+ if (!target.lastMessages) {
89
90
  return "Recipient does not support last-message reads; reload its extension.";
90
91
  }
91
92
  if ("messageId" in query &&
92
- (!targets[0].receiptReads || targets[0].info.rootSessionId !== query.rootSessionId)) {
93
+ (!target.receiptReads || target.info.rootSessionId !== query.rootSessionId)) {
93
94
  return "publication_unavailable";
94
95
  }
95
96
  if (pending >= 128) {
@@ -135,19 +136,19 @@ export class LastMessageRelay {
135
136
  request(fromId, input, reader) {
136
137
  const message = lastRequest(input);
137
138
  const targets = findSessions(this.sessions, message.to, fromId);
138
- const target = targets[0];
139
139
  const from = this.sessions.get(fromId);
140
140
  if (!from) {
141
141
  return;
142
142
  }
143
143
  const token = randomUUID();
144
- const via = targets.length === 1 ? reader(target.info.id) : undefined;
144
+ const delegated = this.delegatedReader(targets, reader);
145
+ const via = delegated?.via;
145
146
  const frame = {
146
147
  type: "last_request",
147
148
  requestId: token,
148
149
  from: messageSender(from.info),
149
150
  query: message.query,
150
- parked: via && messageSender(target.info),
151
+ parked: delegated === undefined ? undefined : messageSender(delegated.target.info),
151
152
  };
152
153
  const error = unavailable(targets, message.to, this.pending.size, { query: message.query, via }) ??
153
154
  validateIntercomMessageSize(frame)?.message;
@@ -159,22 +160,39 @@ export class LastMessageRelay {
159
160
  });
160
161
  return;
161
162
  }
162
- const timer = setTimeout(() => this.finish(token, { error: "Last-message read timed out" }), 5000);
163
+ const [recipient] = targets;
164
+ if (recipient === undefined) {
165
+ return;
166
+ }
167
+ this.forward({ token, fromId, requestId: message.requestId, query: message.query }, recipient, via, frame);
168
+ }
169
+ /** Register the correlated wait, then forward the read to the reader or the recipient. */
170
+ forward(wait, recipient, via, frame) {
171
+ const timer = setTimeout(() => this.finish(wait.token, { error: "Last-message read timed out" }), 5000);
163
172
  timer.unref();
164
- this.pending.set(token, {
165
- fromId,
166
- toId: target.info.id,
173
+ this.pending.set(wait.token, {
174
+ fromId: wait.fromId,
175
+ toId: recipient.info.id,
167
176
  via: via?.info.id,
168
- requestId: message.requestId,
169
- query: message.query,
177
+ requestId: wait.requestId,
178
+ query: wait.query,
170
179
  timer,
171
180
  });
172
181
  try {
173
- writeMessage((via ?? target).socket, frame);
182
+ writeMessage((via ?? recipient).socket, frame);
174
183
  }
175
184
  catch {
176
- this.finish(token, { error: "Recipient disconnected before last-message read" });
185
+ this.finish(wait.token, { error: "Recipient disconnected before last-message read" });
186
+ }
187
+ }
188
+ /** A sole matched target may delegate the read to another peer's captured history. */
189
+ delegatedReader(targets, reader) {
190
+ const [target] = targets;
191
+ if (targets.length !== 1 || target === undefined) {
192
+ return undefined;
177
193
  }
194
+ const via = reader(target.info.id);
195
+ return via === undefined ? undefined : { target, via };
178
196
  }
179
197
  respond(fromId, message) {
180
198
  if (typeof message.requestId !== "string") {
@@ -79,7 +79,7 @@ export class ParkedSessions {
79
79
  return { id: randomUUID() };
80
80
  }
81
81
  previous?.socket.destroy();
82
- return { id, resumed: previous };
82
+ return { id, ...(previous !== undefined ? { resumed: previous } : {}) };
83
83
  }
84
84
  /** Flushes a restored child's input and re-sends wakes owed to a (re)registered main. */
85
85
  registered(id, previous) {
@@ -47,6 +47,7 @@ export class IntercomOutbound {
47
47
  case "queue":
48
48
  return " (intentionally deferred behind active recipient work)";
49
49
  case undefined:
50
+ default:
50
51
  return " (defaults to steer; wakes idle recipients and steers active recipients after the current tool call)";
51
52
  }
52
53
  }
@@ -44,8 +44,9 @@ function explicitReply(contexts, replyTo, to) {
44
44
  }
45
45
  function replyBySender(contexts, to) {
46
46
  const matches = matchingSender(contexts, to);
47
- if (matches.length === 1) {
48
- return matches[0];
47
+ const [onlyMatch] = matches;
48
+ if (matches.length === 1 && onlyMatch !== undefined) {
49
+ return onlyMatch;
49
50
  }
50
51
  if (matches.length > 1) {
51
52
  throw new RecoveryError("ambiguous_reply_target", `Multiple pending asks from "${to}" — use one of: ${pendingSenderOptions(matches, contexts)}.`);
@@ -113,7 +114,7 @@ export class ReplyTracker {
113
114
  const beforeQueued = this.pendingTurnContexts.length;
114
115
  for (let index = this.pendingTurnContexts.length - 1; index >= 0; index -= 1) {
115
116
  const context = this.pendingTurnContexts[index];
116
- if (expires(context)) {
117
+ if (context !== undefined && expires(context)) {
117
118
  this.pendingTurnContexts.splice(index, 1);
118
119
  }
119
120
  }
@@ -145,11 +146,16 @@ export class ReplyTracker {
145
146
  if (to !== "") {
146
147
  return replyBySender(contexts, to);
147
148
  }
149
+ return this.implicitReplyTarget(pending);
150
+ }
151
+ /** Without an explicit target, the turn context wins, then a sole pending ask. */
152
+ implicitReplyTarget(pending) {
148
153
  if (this.currentTurnContext) {
149
154
  return this.currentTurnContext;
150
155
  }
151
- if (pending.length === 1) {
152
- return pending[0];
156
+ const [solePending] = pending;
157
+ if (pending.length === 1 && solePending !== undefined) {
158
+ return solePending;
153
159
  }
154
160
  if (pending.length === 0) {
155
161
  throw new RecoveryError("no_pending_reply", "No active intercom context to reply to");
@@ -1,10 +1,10 @@
1
1
  export interface TargetIdentity {
2
2
  readonly id: string;
3
- readonly name?: string;
3
+ readonly name?: string | undefined;
4
4
  }
5
5
  export interface ProjectSessionIdentity extends TargetIdentity {
6
6
  readonly cwd: string;
7
- readonly projectId?: string;
7
+ readonly projectId?: string | undefined;
8
8
  }
9
9
  export declare const MIN_SESSION_TARGET_PREFIX_LENGTH = 8;
10
10
  export declare function nativePeerId(sessionId: string): string;
@@ -97,8 +97,9 @@ export function formatPeerAwarenessHint(sessions, currentSessionId) {
97
97
  : undefined;
98
98
  }
99
99
  function matchedTargets(matches) {
100
- if (matches.length === 1) {
101
- return { status: "found", target: matches[0], matches };
100
+ const [target] = matches;
101
+ if (matches.length === 1 && target !== undefined) {
102
+ return { status: "found", target, matches };
102
103
  }
103
104
  return matches.length > 1 ? { status: "ambiguous", matches } : { status: "none", matches: [] };
104
105
  }
@@ -9,6 +9,7 @@ export function registerIntercomTools(pi, tools) {
9
9
  description: "Message connected Pi sessions in your group; mains also reach other mains across projects. List shows target IDs. Ask waits for a correlated reply; reply answers the active or sole pending ask. Pending lists asks; status shows connection and inbound queue. Last pages a peer's last completed response without waking it. Broker acceptance is not recipient persistence, consumption or completion.",
10
10
  parameters: intercomSchema,
11
11
  renderResult: renderCompactResult,
12
+ // oxlint-disable-next-line max-params
12
13
  async execute(_id, params, signal, _update, ctx) {
13
14
  const result = throwIfToolError(await tools.execute(params, signal, ctx));
14
15
  return { content: [...result.content], details: result.details };