@ian-pascoe/pi-minimal-subagents 0.2.1 → 0.2.3

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.
package/README.md CHANGED
@@ -141,26 +141,29 @@ the complete read-only discovery bundle.
141
141
  `agent_message` reports whether a message was delivered through an active
142
142
  parent wait, queued for the recipient, or failed. `subagent_wait` can return an
143
143
  intermediate Wait Event containing a Coordination Message before the child turn
144
- settles; call it again for the terminal turn result. Pass optional `turn_id` to
145
- address an older retained turn exactly. Without it, waits select the oldest
146
- observable claimed or pending turn before the active/latest turn. A caller may
147
- have only one outstanding wait for the same source turn; a concurrent duplicate
148
- is rejected instead of competing for one Wait Event.
144
+ settles. That event claims only its message, so later unconsumed messages and the
145
+ terminal result retain automatic fallback. If the turn has already settled, one
146
+ wait returns its terminal result with queued messages in `messages`. Pass
147
+ optional `turn_id` to address an older retained turn exactly. Without it, waits
148
+ select the oldest observable claimed or pending turn before the active/latest
149
+ turn. A caller may have only one outstanding wait for the same source turn; a
150
+ concurrent duplicate is rejected instead of competing for one Wait Event.
149
151
 
150
152
  The persisted Delivery Ledger records Coordination Messages, terminal results,
151
153
  globally increasing sequence, and wait ownership before delivery. Existing
152
154
  items retain their sequence; gaps from skipped malformed records are valid.
153
- Claims can name only active, latest, or retained turns. Once a wait returns an
154
- intermediate message, that wait path owns the rest of the source turn across
155
- reloads, forks, and newer turns. Automatic fallback retains its ordered queue
156
- reservation, treats idle notifications as advisory, and rechecks actual
157
- recipient idleness before injecting a message. Destination-session Delivery
158
- Evidence settles and compacts ledger items, preventing duplicate delivery and
155
+ Claims can name only active, latest, or retained turns. Wait-returned messages
156
+ retain individual delivery evidence; terminal wait ownership remains durable
157
+ across reloads, forks, and newer turns. Automatic fallback retains its ordered
158
+ queue reservation while batching queued messages from one source turn into one
159
+ Pi steer. Root-bound messages remain batchable while the root turn is active; a
160
+ pending terminal result absorbs them. Child sessions drain all available steers
161
+ before the next model call. Destination-session Delivery Evidence still settles
162
+ each batched ledger item independently, preventing duplicate delivery and
159
163
  unbounded checkpoint growth. The pure Delivery Ledger state machine retains at
160
- most 20 pending wait-only terminal results per source agent; Coordination
161
- Messages are not removed by that terminal-retention limit. Delivered messages
162
- include stable delivery, source-agent, and source-turn identities in persisted
163
- details.
164
+ most 20 pending wait-only terminal results per source agent; Coordination Messages
165
+ are not removed by that terminal-retention limit. Delivered messages include
166
+ stable delivery, source-agent, and source-turn identities in persisted details.
164
167
 
165
168
  Deleting a child first verifies its session header and persistent identity,
166
169
  then uses the optional `trash` command when available and falls back to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-minimal-subagents",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "description": "Persistent nested subagents with bounded delegation for Pi",
6
6
  "keywords": [
@@ -53,6 +53,7 @@ import type {
53
53
  StatusResult,
54
54
  TurnId,
55
55
  TurnResult,
56
+ WaitMessageResult,
56
57
  WaitResult,
57
58
  } from "./minimal-subagents-types.js";
58
59
 
@@ -85,11 +86,6 @@ interface PendingParentMessage {
85
86
  cancelGrace?: () => void;
86
87
  }
87
88
 
88
- interface CancelableWait {
89
- promise: Promise<void>;
90
- cancel: () => void;
91
- }
92
-
93
89
  function agentDeliveryKey(agentId: string, turnId: string): string {
94
90
  return `${agentId}\u0000${turnId}`;
95
91
  }
@@ -109,8 +105,27 @@ function terminalTurnResult(
109
105
  };
110
106
  }
111
107
 
112
- function terminalWaitResult(result: TurnResult): WaitResult {
113
- return { event: "turn", ...structuredClone(result) };
108
+ function terminalWaitResult(result: TurnResult, messages: WaitMessageResult[] = []): WaitResult {
109
+ const terminal = { event: "turn" as const, ...structuredClone(result) };
110
+ return messages.length === 0 ? terminal : { ...terminal, messages: structuredClone(messages) };
111
+ }
112
+
113
+ function combineCoordinatorMessages(messages: readonly CoordinatorMessage[]): CoordinatorMessage {
114
+ const latest = messages.at(-1);
115
+ if (!latest) throw new Error("Minimal subagents message batch must not be empty");
116
+ if (messages.length === 1) return latest;
117
+ const references = messages.flatMap(
118
+ (message) =>
119
+ message.details.messages ??
120
+ (message.details.delivery_id
121
+ ? [{ delivery_id: message.details.delivery_id, message_id: message.details.message_id }]
122
+ : []),
123
+ );
124
+ return {
125
+ ...latest,
126
+ content: messages.map((message) => message.content).join("\n\n"),
127
+ details: references.length > 0 ? { ...latest.details, messages: references } : latest.details,
128
+ };
114
129
  }
115
130
 
116
131
  /** One root-owned coordinator for persistent nested Pi child sessions. */
@@ -124,11 +139,9 @@ export class MinimalSubagentsCoordinator {
124
139
  private readonly waiters = new Map<string, Set<TurnWaiter>>();
125
140
  private readonly pendingParentMessages = new Map<string, PendingParentMessage[]>();
126
141
  private readonly recipientQueues = new Map<string, Promise<unknown>>();
127
- private readonly recipientIdleWaiters = new Map<string, Set<() => void>>();
128
142
  private readonly automaticDeliveryKeys = new Set<string>();
129
143
  private readonly automaticCoordinationDeliveryIds = new Set<string>();
130
144
  private readonly waitHandedDeliveryIds = new Set<string>();
131
- private readonly automaticDeliveryClaimWaiters = new Map<string, Set<() => void>>();
132
145
  private readonly backgroundOperations = new Set<Promise<void>>();
133
146
  private acceptingOperations = true;
134
147
  private lifecycleEpoch = 0;
@@ -363,15 +376,16 @@ export class MinimalSubagentsCoordinator {
363
376
  new Error(`Minimal subagents duplicate wait: ${callerId} is already waiting for ${turnId}`),
364
377
  );
365
378
  }
366
- const pendingMessage = this.claimPendingParentMessage(callerId, agentId, turnId);
367
- if (pendingMessage) return Promise.resolve(pendingMessage);
368
379
  const retainedResult =
369
380
  findTerminalDelivery(this.deliveryLedger, agentId, turnId)?.result ??
370
381
  (agent.latest_result?.turn_id === turnId ? agent.latest_result : undefined);
371
382
  if (retainedResult) {
383
+ const messages = this.drainPendingParentMessages(callerId, agentId, turnId);
372
384
  this.claimTerminalDelivery(callerId, retainedResult);
373
- return Promise.resolve(terminalWaitResult(retainedResult));
385
+ return Promise.resolve(terminalWaitResult(retainedResult, messages));
374
386
  }
387
+ const pendingMessage = this.claimPendingParentMessage(callerId, agentId, turnId);
388
+ if (pendingMessage) return Promise.resolve(pendingMessage);
375
389
  if (agent.active_turn_id !== turnId) {
376
390
  return Promise.reject(
377
391
  new Error(`Minimal subagents wait: turn ${turnId} is no longer retained for ${agentId}`),
@@ -543,7 +557,6 @@ export class MinimalSubagentsCoordinator {
543
557
  this.agents.clear();
544
558
  this.deliveryLedger = createDeliveryLedger();
545
559
  this.pendingParentMessages.clear();
546
- this.releaseAllRecipientIdleWaiters();
547
560
  this.recipientQueues.clear();
548
561
  this.backgroundOperations.clear();
549
562
  await Promise.allSettled(
@@ -558,12 +571,10 @@ export class MinimalSubagentsCoordinator {
558
571
  this.waiters.clear();
559
572
  this.pendingParentMessages.clear();
560
573
  this.waitHandedDeliveryIds.clear();
561
- this.releaseAllRecipientIdleWaiters();
562
574
  this.recipientQueues.clear();
563
575
  this.backgroundOperations.clear();
564
576
  this.automaticDeliveryKeys.clear();
565
577
  this.automaticCoordinationDeliveryIds.clear();
566
- this.automaticDeliveryClaimWaiters.clear();
567
578
  this.deliveryLedger = createDeliveryLedger({
568
579
  deliveries: snapshot.deliveries,
569
580
  coordination_deliveries: snapshot.coordination_deliveries,
@@ -720,13 +731,6 @@ export class MinimalSubagentsCoordinator {
720
731
  await Promise.allSettled(scheduled);
721
732
  }
722
733
 
723
- /** Release ordered automatic deliveries after one recipient conversation becomes idle. */
724
- markRecipientIdle(agentId: string): void {
725
- const waiters = this.recipientIdleWaiters.get(agentId);
726
- this.recipientIdleWaiters.delete(agentId);
727
- for (const resolve of waiters ?? []) resolve();
728
- }
729
-
730
734
  /** Clone complete child leaves for root fork ownership without ever sharing source session paths. */
731
735
  async prepareFork(sourceRootSessionFile: string): Promise<ForkSnapshot> {
732
736
  const activeRootChildren = this.childrenOf("root");
@@ -785,7 +789,6 @@ export class MinimalSubagentsCoordinator {
785
789
  shutdown(): Promise<void> {
786
790
  if (this.shutdownPromise) return this.shutdownPromise;
787
791
  this.acceptingOperations = false;
788
- this.releaseAllRecipientIdleWaiters();
789
792
  this.shutdownPromise = this.finishShutdown();
790
793
  return this.shutdownPromise;
791
794
  }
@@ -999,7 +1002,6 @@ export class MinimalSubagentsCoordinator {
999
1002
  });
1000
1003
  }
1001
1004
  if (result.status !== "completed") this.removeSettledEmptyTurnClaim(agent.agent_id, turnId);
1002
- this.markRecipientIdle(agent.agent_id);
1003
1005
  }
1004
1006
 
1005
1007
  private async deliverAutomaticResult(
@@ -1010,6 +1012,7 @@ export class MinimalSubagentsCoordinator {
1010
1012
  if (this.automaticDeliveryKeys.has(deliveryKey)) return;
1011
1013
  this.automaticDeliveryKeys.add(deliveryKey);
1012
1014
  const graceMs = this.deliveryGraceMs();
1015
+ let batchedCoordinationDeliveries: PersistedCoordinationDelivery[] = [];
1013
1016
  try {
1014
1017
  await this.enqueueRecipientDelivery(delivery.destination_agent_id, async () => {
1015
1018
  if (delivery.destination_agent_id !== "root") {
@@ -1024,15 +1027,6 @@ export class MinimalSubagentsCoordinator {
1024
1027
  this.settleDelivery(delivery);
1025
1028
  return;
1026
1029
  }
1027
- while (!this.isRecipientIdle(delivery.destination_agent_id)) {
1028
- const idleWait = this.createRecipientIdleWait(delivery.destination_agent_id);
1029
- const claimWait = this.createAutomaticDeliveryClaimWait(deliveryKey);
1030
- await Promise.race([idleWait.promise, claimWait.promise]);
1031
- idleWait.cancel();
1032
- claimWait.cancel();
1033
- if (!this.acceptingOperations || this.shouldStopAutomaticTerminalDelivery(delivery))
1034
- return;
1035
- }
1036
1030
  if (!this.acceptingOperations || this.shouldStopAutomaticTerminalDelivery(delivery)) return;
1037
1031
  if (this.hasDeliveryEvidence(delivery)) {
1038
1032
  this.settleDelivery(delivery);
@@ -1051,14 +1045,28 @@ export class MinimalSubagentsCoordinator {
1051
1045
  usage: result.usage,
1052
1046
  },
1053
1047
  };
1048
+ batchedCoordinationDeliveries = this.takePendingParentDeliveryBatch(
1049
+ deliveryKey,
1050
+ delivery.destination_agent_id,
1051
+ );
1052
+ for (const batchedDelivery of batchedCoordinationDeliveries) {
1053
+ this.waitHandedDeliveryIds.add(batchedDelivery.delivery_id);
1054
+ }
1054
1055
  await this.deliverToRecipient(
1055
1056
  delivery.destination_agent_id,
1056
- message,
1057
- () => this.isTerminalDeliveryCurrent(delivery),
1058
- true,
1057
+ combineCoordinatorMessages([
1058
+ ...batchedCoordinationDeliveries.map((item) => item.message),
1059
+ message,
1060
+ ]),
1061
+ () =>
1062
+ this.isTerminalDeliveryCurrent(delivery) &&
1063
+ batchedCoordinationDeliveries.every((item) => this.isCoordinationDeliveryCurrent(item)),
1059
1064
  );
1060
1065
  });
1061
1066
  } catch (error) {
1067
+ for (const batchedDelivery of batchedCoordinationDeliveries) {
1068
+ this.waitHandedDeliveryIds.delete(batchedDelivery.delivery_id);
1069
+ }
1062
1070
  if (!this.isTerminalDeliveryCurrent(delivery)) return;
1063
1071
  const deliveryError = error instanceof Error ? error.message : String(error);
1064
1072
  this.deliveryLedger = setTerminalDeliveryError(
@@ -1263,7 +1271,6 @@ export class MinimalSubagentsCoordinator {
1263
1271
  targetId: string,
1264
1272
  message: CoordinatorMessage,
1265
1273
  isCurrentDelivery: () => boolean = () => true,
1266
- requireIdleRecipient = false,
1267
1274
  ): Promise<void> {
1268
1275
  if (!isCurrentDelivery()) {
1269
1276
  throw new Error("Minimal subagents delivery abandoned after session branch change");
@@ -1281,9 +1288,6 @@ export class MinimalSubagentsCoordinator {
1281
1288
  throw new Error("Minimal subagents delivery abandoned after session branch change");
1282
1289
  }
1283
1290
  const visibleMessage = addCoordinatorMessageEnvelope(message);
1284
- if (requireIdleRecipient && (target.active_turn_id || runtime.isRunning)) {
1285
- throw new Error(`Minimal subagents automatic delivery recipient became active: ${targetId}`);
1286
- }
1287
1291
  if (target.active_turn_id || runtime.isRunning) {
1288
1292
  await runtime.queueCoordinatorMessage(visibleMessage);
1289
1293
  return;
@@ -1367,8 +1371,6 @@ export class MinimalSubagentsCoordinator {
1367
1371
  private applyDeliveryLedgerTransition(transition: DeliveryLedgerTransition): void {
1368
1372
  this.deliveryLedger = transition.ledger;
1369
1373
  for (const delivery of transition.prunedTerminalDeliveries) {
1370
- const key = agentDeliveryKey(delivery.source_agent_id, delivery.source_turn_id);
1371
- this.releaseAutomaticDeliveryClaimWaiters(key);
1372
1374
  this.dependencies.registry.append(
1373
1375
  createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pruned", {
1374
1376
  source_agent_id: delivery.source_agent_id,
@@ -1504,16 +1506,8 @@ export class MinimalSubagentsCoordinator {
1504
1506
  }
1505
1507
 
1506
1508
  private pruneDeliveryStateForDeletedAgent(agentId: string): void {
1507
- const previousTerminalDeliveries = this.deliveryLedger.terminalDeliveries;
1508
1509
  const previousCoordinationDeliveries = this.deliveryLedger.coordinationDeliveries;
1509
1510
  this.deliveryLedger = pruneDeliveryLedgerAgents(this.deliveryLedger, [agentId]).ledger;
1510
- for (const delivery of previousTerminalDeliveries) {
1511
- if (!this.isTerminalDeliveryCurrent(delivery)) {
1512
- this.releaseAutomaticDeliveryClaimWaiters(
1513
- agentDeliveryKey(delivery.source_agent_id, delivery.source_turn_id),
1514
- );
1515
- }
1516
- }
1517
1511
  for (const delivery of previousCoordinationDeliveries) {
1518
1512
  if (!this.isCoordinationDeliveryCurrent(delivery)) {
1519
1513
  this.waitHandedDeliveryIds.delete(delivery.delivery_id);
@@ -1673,7 +1667,6 @@ export class MinimalSubagentsCoordinator {
1673
1667
  (candidate) => candidate.callerId === destinationAgentId,
1674
1668
  );
1675
1669
  if (!waiter) return false;
1676
- this.claimDeliveryTurn(message.details.source_agent_id, message.details.source_turn_id);
1677
1670
  this.deliveryLedger = setCoordinationDeliveryPath(
1678
1671
  this.deliveryLedger,
1679
1672
  delivery.delivery_id,
@@ -1698,7 +1691,7 @@ export class MinimalSubagentsCoordinator {
1698
1691
  callerId: string,
1699
1692
  sourceAgentId: string,
1700
1693
  sourceTurnId: string,
1701
- ): WaitResult | undefined {
1694
+ ): WaitMessageResult | undefined {
1702
1695
  const key = agentDeliveryKey(sourceAgentId, sourceTurnId);
1703
1696
  const pendingMessages = this.pendingParentMessages.get(key);
1704
1697
  const index = pendingMessages?.findIndex(
@@ -1715,7 +1708,6 @@ export class MinimalSubagentsCoordinator {
1715
1708
  )
1716
1709
  .sort((left, right) => left.sequence - right.sequence)[0];
1717
1710
  if (!retained) return undefined;
1718
- this.claimDeliveryTurn(sourceAgentId, sourceTurnId);
1719
1711
  this.deliveryLedger = setCoordinationDeliveryPath(
1720
1712
  this.deliveryLedger,
1721
1713
  retained.delivery_id,
@@ -1735,7 +1727,6 @@ export class MinimalSubagentsCoordinator {
1735
1727
  }
1736
1728
  const pending = pendingMessages[index];
1737
1729
  if (!pending) return undefined;
1738
- this.claimDeliveryTurn(sourceAgentId, sourceTurnId);
1739
1730
  const delivery = findCoordinationDelivery(this.deliveryLedger, pending.deliveryId);
1740
1731
  if (delivery) {
1741
1732
  this.deliveryLedger = setCoordinationDeliveryPath(
@@ -1747,10 +1738,6 @@ export class MinimalSubagentsCoordinator {
1747
1738
  if (currentDelivery) this.persistCoordinationDeliveryState(currentDelivery);
1748
1739
  this.waitHandedDeliveryIds.add(delivery.delivery_id);
1749
1740
  }
1750
- const sourceResult = this.agents.get(sourceAgentId)?.latest_result;
1751
- if (sourceResult?.turn_id === sourceTurnId) {
1752
- this.setTerminalDeliveryPathToWait(callerId, sourceResult);
1753
- }
1754
1741
  pending.claimed = true;
1755
1742
  pending.cancelGrace?.();
1756
1743
  pending.releaseClaim();
@@ -1766,6 +1753,20 @@ export class MinimalSubagentsCoordinator {
1766
1753
  };
1767
1754
  }
1768
1755
 
1756
+ private drainPendingParentMessages(
1757
+ callerId: string,
1758
+ sourceAgentId: string,
1759
+ sourceTurnId: string,
1760
+ ): WaitMessageResult[] {
1761
+ const messages: WaitMessageResult[] = [];
1762
+ let message = this.claimPendingParentMessage(callerId, sourceAgentId, sourceTurnId);
1763
+ while (message) {
1764
+ messages.push(message);
1765
+ message = this.claimPendingParentMessage(callerId, sourceAgentId, sourceTurnId);
1766
+ }
1767
+ return messages;
1768
+ }
1769
+
1769
1770
  private queuePendingParentMessage(
1770
1771
  targetId: string,
1771
1772
  message: CoordinatorMessage,
@@ -1800,6 +1801,7 @@ export class MinimalSubagentsCoordinator {
1800
1801
  )
1801
1802
  return;
1802
1803
 
1804
+ let automaticBatch = [delivery];
1803
1805
  const operation = this.enqueueRecipientDelivery(targetId, async () => {
1804
1806
  if (targetId !== "root") {
1805
1807
  await this.ensureRuntime(this.requireUsableAgent(targetId, "message"));
@@ -1821,34 +1823,63 @@ export class MinimalSubagentsCoordinator {
1821
1823
  pending.cancelGrace?.();
1822
1824
  pending.cancelGrace = undefined;
1823
1825
  if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
1824
- while (!this.isRecipientIdle(targetId)) {
1825
- const idleWait = this.createRecipientIdleWait(targetId);
1826
- await Promise.race([pending.claimPromise, idleWait.promise]);
1827
- idleWait.cancel();
1828
- if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
1826
+ while (
1827
+ targetId === "root" &&
1828
+ !this.dependencies.root.isIdle() &&
1829
+ !findTerminalDelivery(
1830
+ this.deliveryLedger,
1831
+ message.details.source_agent_id,
1832
+ message.details.source_turn_id,
1833
+ ) &&
1834
+ this.acceptingOperations &&
1835
+ !pending.claimed &&
1836
+ !turnClaimed()
1837
+ ) {
1838
+ await Promise.race([
1839
+ pending.claimPromise,
1840
+ new Promise((resolve) => setTimeout(resolve, 25)),
1841
+ ]);
1829
1842
  }
1830
1843
  if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
1831
- this.removePendingParentMessage(key, pending);
1832
- this.waitHandedDeliveryIds.add(delivery.delivery_id);
1844
+ const terminalDelivery = findTerminalDelivery(
1845
+ this.deliveryLedger,
1846
+ message.details.source_agent_id,
1847
+ message.details.source_turn_id,
1848
+ );
1849
+ if (
1850
+ terminalDelivery?.destination_agent_id === targetId &&
1851
+ terminalDelivery.path === "message"
1852
+ ) {
1853
+ for (const queued of this.pendingParentMessages.get(key) ?? []) {
1854
+ queued.cancelGrace?.();
1855
+ queued.releaseClaim();
1856
+ }
1857
+ return;
1858
+ }
1859
+ automaticBatch = this.takePendingParentDeliveryBatch(key, targetId);
1860
+ for (const batchedDelivery of automaticBatch) {
1861
+ this.waitHandedDeliveryIds.add(batchedDelivery.delivery_id);
1862
+ }
1863
+ if (automaticBatch.length === 0) return;
1833
1864
  await this.deliverToRecipient(
1834
1865
  targetId,
1835
- message,
1836
- () => this.isCoordinationDeliveryCurrent(delivery),
1837
- true,
1866
+ combineCoordinatorMessages(automaticBatch.map((item) => item.message)),
1867
+ () => automaticBatch.every((item) => this.isCoordinationDeliveryCurrent(item)),
1838
1868
  );
1839
1869
  });
1840
1870
  void operation.catch((cause) => {
1841
- this.removePendingParentMessage(key, pending);
1842
- this.waitHandedDeliveryIds.delete(delivery.delivery_id);
1843
- if (!this.isCoordinationDeliveryCurrent(delivery)) return;
1844
1871
  const deliveryError = cause instanceof Error ? cause.message : String(cause);
1845
- this.deliveryLedger = setCoordinationDeliveryError(
1846
- this.deliveryLedger,
1847
- delivery.delivery_id,
1848
- deliveryError,
1849
- );
1850
- const current = findCoordinationDelivery(this.deliveryLedger, delivery.delivery_id);
1851
- if (current) this.persistCoordinationDeliveryState(current);
1872
+ for (const batchedDelivery of automaticBatch) {
1873
+ this.waitHandedDeliveryIds.delete(batchedDelivery.delivery_id);
1874
+ if (!this.isCoordinationDeliveryCurrent(batchedDelivery)) continue;
1875
+ this.deliveryLedger = setCoordinationDeliveryError(
1876
+ this.deliveryLedger,
1877
+ batchedDelivery.delivery_id,
1878
+ deliveryError,
1879
+ );
1880
+ const current = findCoordinationDelivery(this.deliveryLedger, batchedDelivery.delivery_id);
1881
+ if (current) this.persistCoordinationDeliveryState(current);
1882
+ }
1852
1883
  this.dependencies.notify?.({
1853
1884
  type: "failure",
1854
1885
  agentId: message.details.source_agent_id,
@@ -1859,6 +1890,25 @@ export class MinimalSubagentsCoordinator {
1859
1890
  });
1860
1891
  }
1861
1892
 
1893
+ private takePendingParentDeliveryBatch(
1894
+ key: string,
1895
+ targetId: string,
1896
+ ): PersistedCoordinationDelivery[] {
1897
+ const batch = (this.pendingParentMessages.get(key) ?? []).filter(
1898
+ (pending) => pending.destinationAgentId === targetId && !pending.claimed,
1899
+ );
1900
+ for (const pending of batch) {
1901
+ pending.claimed = true;
1902
+ pending.cancelGrace?.();
1903
+ pending.releaseClaim();
1904
+ this.removePendingParentMessage(key, pending);
1905
+ }
1906
+ return batch.flatMap((pending) => {
1907
+ const delivery = findCoordinationDelivery(this.deliveryLedger, pending.deliveryId);
1908
+ return delivery ? [delivery] : [];
1909
+ });
1910
+ }
1911
+
1862
1912
  private removePendingParentMessage(key: string, pending: PendingParentMessage): void {
1863
1913
  const pendingMessages = this.pendingParentMessages.get(key);
1864
1914
  if (!pendingMessages) return;
@@ -1874,14 +1924,12 @@ export class MinimalSubagentsCoordinator {
1874
1924
  }
1875
1925
 
1876
1926
  private setTerminalDeliveryPathToWait(callerId: string, result: TurnResult): void {
1877
- const key = agentDeliveryKey(result.agent_id, result.turn_id);
1878
1927
  const delivery = findTerminalDelivery(this.deliveryLedger, result.agent_id, result.turn_id);
1879
1928
  if (!delivery || delivery.destination_agent_id !== callerId || delivery.path === "wait") return;
1880
1929
  this.applyDeliveryLedgerTransition(
1881
1930
  setTerminalDeliveryPath(this.deliveryLedger, result.agent_id, result.turn_id, "wait"),
1882
1931
  );
1883
1932
  const retained = findTerminalDelivery(this.deliveryLedger, result.agent_id, result.turn_id);
1884
- this.releaseAutomaticDeliveryClaimWaiters(key);
1885
1933
  if (!retained) return;
1886
1934
  this.dependencies.registry.append(
1887
1935
  createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pending", {
@@ -1890,57 +1938,6 @@ export class MinimalSubagentsCoordinator {
1890
1938
  );
1891
1939
  }
1892
1940
 
1893
- private isRecipientIdle(agentId: string): boolean {
1894
- if (agentId === "root") return this.dependencies.root.isIdle();
1895
- const agent = this.agents.get(agentId);
1896
- if (!agent || agent.active_turn_id) return false;
1897
- return !this.runtimes.get(agentId)?.isRunning;
1898
- }
1899
-
1900
- private createRecipientIdleWait(agentId: string): CancelableWait {
1901
- const { promise, resolve: release } = Promise.withResolvers<void>();
1902
- const waiters = this.recipientIdleWaiters.get(agentId) ?? new Set();
1903
- waiters.add(release);
1904
- this.recipientIdleWaiters.set(agentId, waiters);
1905
- return {
1906
- promise,
1907
- cancel: () => {
1908
- const currentWaiters = this.recipientIdleWaiters.get(agentId);
1909
- currentWaiters?.delete(release);
1910
- if (currentWaiters?.size === 0) this.recipientIdleWaiters.delete(agentId);
1911
- },
1912
- };
1913
- }
1914
-
1915
- private createAutomaticDeliveryClaimWait(deliveryKey: string): CancelableWait {
1916
- const { promise, resolve: release } = Promise.withResolvers<void>();
1917
- const waiters = this.automaticDeliveryClaimWaiters.get(deliveryKey) ?? new Set();
1918
- waiters.add(release);
1919
- this.automaticDeliveryClaimWaiters.set(deliveryKey, waiters);
1920
- return {
1921
- promise,
1922
- cancel: () => {
1923
- const currentWaiters = this.automaticDeliveryClaimWaiters.get(deliveryKey);
1924
- currentWaiters?.delete(release);
1925
- if (currentWaiters?.size === 0) this.automaticDeliveryClaimWaiters.delete(deliveryKey);
1926
- },
1927
- };
1928
- }
1929
-
1930
- private releaseAutomaticDeliveryClaimWaiters(deliveryKey: string): void {
1931
- const waiters = this.automaticDeliveryClaimWaiters.get(deliveryKey);
1932
- this.automaticDeliveryClaimWaiters.delete(deliveryKey);
1933
- for (const resolve of waiters ?? []) resolve();
1934
- }
1935
-
1936
- private releaseAllRecipientIdleWaiters(): void {
1937
- const allWaiters = [...this.recipientIdleWaiters.values()];
1938
- this.recipientIdleWaiters.clear();
1939
- for (const waiters of allWaiters) {
1940
- for (const resolve of waiters) resolve();
1941
- }
1942
- }
1943
-
1944
1941
  private async cancelDuringShutdown(agentId: string): Promise<void> {
1945
1942
  const target = this.agents.get(agentId);
1946
1943
  if (!target) return;
@@ -5,7 +5,6 @@ import {
5
5
  getAgentDir,
6
6
  SessionManager,
7
7
  SettingsManager,
8
- type AgentSettledEvent,
9
8
  type ExtensionAPI,
10
9
  type ExtensionContext,
11
10
  type ExtensionFactory,
@@ -100,6 +99,7 @@ function createRootConversationEndpoint(
100
99
  },
101
100
  );
102
101
  },
102
+ isIdle: () => context.isIdle(),
103
103
  hasDeliveryEvidence: (sourceAgentId, sourceTurnId, deliveryId) =>
104
104
  findDeliveryEvidence(
105
105
  context.sessionManager.getBranch(),
@@ -107,7 +107,6 @@ function createRootConversationEndpoint(
107
107
  sourceTurnId,
108
108
  deliveryId,
109
109
  ),
110
- isIdle: () => context.isIdle(),
111
110
  };
112
111
  }
113
112
 
@@ -331,7 +330,6 @@ export class MinimalSubagentsLifecycleController {
331
330
  this.pi.on("session_before_fork", (event, context) => this.prepareSessionFork(event, context));
332
331
  this.pi.on("session_tree", (event, context) => this.restoreSessionTree(event, context));
333
332
  this.pi.on("message_end", (event, context) => this.reconcileMessageDelivery(event, context));
334
- this.pi.on("agent_settled", (event, context) => this.releaseSettledRecipient(event, context));
335
333
  this.pi.on("session_shutdown", (event, context) => this.shutdownSession(event, context));
336
334
  }
337
335
 
@@ -554,12 +552,6 @@ export class MinimalSubagentsLifecycleController {
554
552
  }
555
553
  }
556
554
 
557
- private releaseSettledRecipient(_event: AgentSettledEvent, context: ExtensionContext): void {
558
- if (!this.coordinator) return;
559
- if (context.isIdle()) this.coordinator.markRecipientIdle("root");
560
- this.uiController?.refresh();
561
- }
562
-
563
555
  private async shutdownSession(
564
556
  event: SessionShutdownEvent,
565
557
  context: ExtensionContext,
@@ -132,6 +132,7 @@ const WaitMessageDetailsSchema = Type.Object({
132
132
  agent_id: Type.String(),
133
133
  turn_id: Type.String(),
134
134
  message_id: Type.String(),
135
+ delivery_id: Type.Optional(Type.String()),
135
136
  message: Type.String(),
136
137
  elapsed_ms: Type.Optional(Type.Number()),
137
138
  usage: Type.Optional(RenderUsageSchema),
@@ -145,6 +146,7 @@ const WaitTurnDetailsSchema = Type.Object({
145
146
  error: Type.Optional(Type.String()),
146
147
  elapsed_ms: Type.Optional(Type.Number()),
147
148
  usage: Type.Optional(RenderUsageSchema),
149
+ messages: Type.Optional(Type.Array(WaitMessageDetailsSchema)),
148
150
  });
149
151
  const StatusDetailsSchema = Type.Union([
150
152
  Type.Object({
@@ -341,9 +341,12 @@ function renderWaitResult(
341
341
  : details.status;
342
342
  const duration = formatSubagentDuration(details.elapsed_ms);
343
343
  const tokens = formatSubagentTokenCount(details.usage?.totalTokens);
344
- const metrics = [duration, tokens ? `${tokens} tokens` : undefined].filter(
345
- (metric): metric is string => metric !== undefined,
346
- );
344
+ const drainedMessageCount = details.event === "message" ? 0 : (details.messages?.length ?? 0);
345
+ const metrics = [
346
+ duration,
347
+ tokens ? `${tokens} tokens` : undefined,
348
+ drainedMessageCount > 0 ? `${drainedMessageCount} messages` : undefined,
349
+ ].filter((metric): metric is string => metric !== undefined);
347
350
  const summary = renderSubagentSummary(theme, status, agentId, metrics);
348
351
  if (options.isPartial || !options.expanded) {
349
352
  return new Text(`${summary}${options.isPartial ? "" : collapsedExpansionHint(theme)}`, 0, 0);
@@ -356,6 +359,14 @@ function renderWaitResult(
356
359
  container.addChild(renderLabelValue(theme, "Message ID", details.message_id));
357
360
  return container;
358
361
  }
362
+ if (details.messages && details.messages.length > 0) {
363
+ appendTextSection(
364
+ container,
365
+ theme,
366
+ "Messages",
367
+ details.messages.map((message) => message.message).join("\n\n"),
368
+ );
369
+ }
359
370
  const output = details.output ?? "";
360
371
  if (status === "completed") {
361
372
  if (output.length > 0) {
@@ -28,6 +28,11 @@ export const ForkOwnershipRecordSchema = Type.Object({
28
28
  direct_parent_id: Type.String(),
29
29
  });
30
30
 
31
+ const DeliveryEvidenceMessageSchema = Type.Object({
32
+ delivery_id: Type.Optional(Type.String()),
33
+ message_id: Type.Optional(Type.String()),
34
+ });
35
+
31
36
  /** Parses durable custom-message and wait-tool Delivery Evidence details. */
32
37
  export const DeliveryEvidenceDetailsSchema = Type.Object({
33
38
  event: Type.Optional(Type.String()),
@@ -35,6 +40,7 @@ export const DeliveryEvidenceDetailsSchema = Type.Object({
35
40
  source_turn_id: Type.String(),
36
41
  delivery_id: Type.Optional(Type.String()),
37
42
  message_id: Type.Optional(Type.String()),
43
+ messages: Type.Optional(Type.Array(DeliveryEvidenceMessageSchema)),
38
44
  });
39
45
 
40
46
  export type ChildSessionIdentityRecord = Static<typeof ChildSessionIdentityRecordSchema>;
@@ -411,7 +411,11 @@ export function findDeliveryEvidence(
411
411
  return (
412
412
  details.source_agent_id === sourceAgentId &&
413
413
  details.source_turn_id === sourceTurnId &&
414
- (details.delivery_id === deliveryId || details.message_id === deliveryId)
414
+ (details.delivery_id === deliveryId ||
415
+ details.message_id === deliveryId ||
416
+ details.messages?.some(
417
+ (message) => message.delivery_id === deliveryId || message.message_id === deliveryId,
418
+ ))
415
419
  );
416
420
  }
417
421
  if (waitToolResult && details.event === "message") return false;
@@ -516,6 +520,8 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
516
520
  private readonly modelById: ReadonlyMap<string, Model<any>>,
517
521
  onSessionActivity?: () => void,
518
522
  ) {
523
+ // Keep this child-only; AgentSession.setSteeringMode would overwrite the user's global setting.
524
+ session.agent.steeringMode = "all";
519
525
  this.unsubscribe = session.subscribe((event) => {
520
526
  if (event.type !== "entry_appended") return;
521
527
  if (
@@ -202,7 +202,7 @@ export function createCoordinatorToolDefinitions(
202
202
  name: "subagent_wait",
203
203
  label: "Subagent Wait",
204
204
  description:
205
- "Wait for one direct child's oldest observable turn, or select an exact retained turn_id. A Wait Event containing a Coordination Message may arrive first as event=message; call again for the terminal turn result. A successful wait durably claims that turn so later messages and its terminal result return through wait without duplicate automatic delivery. Timeout never cancels the child.",
205
+ "Wait for one direct child's oldest observable turn, or select an exact retained turn_id. An active child may first return event=message; later unconsumed items still fall back automatically. An already settled turn returns event=turn once with queued messages in messages. Timeout never cancels the child.",
206
206
  promptSnippet: "Wait for one direct child's exact turn",
207
207
  parameters: options.schemas.subagent_wait,
208
208
  async execute(_toolCallId, parameters, signal, onUpdate) {
@@ -75,8 +75,14 @@ export interface WaitMessageResult {
75
75
  message: string;
76
76
  }
77
77
 
78
+ /** Reports one terminal child turn and any earlier messages drained by the same wait. */
79
+ export interface WaitTurnResult extends TurnResult {
80
+ event: "turn";
81
+ messages?: WaitMessageResult[];
82
+ }
83
+
78
84
  /** Reports one terminal child turn returned by subagent_wait. */
79
- export type WaitResult = WaitMessageResult | ({ event: "turn" } & TurnResult);
85
+ export type WaitResult = WaitMessageResult | WaitTurnResult;
80
86
 
81
87
  /** Provides bounded hierarchy, usage, and best-known Runtime Profile data for one persistent agent. */
82
88
  export interface AgentSummary extends RuntimeProfile {
@@ -182,6 +188,7 @@ export interface CoordinatorMessage {
182
188
  status?: TurnStatus;
183
189
  elapsed_ms?: number;
184
190
  usage?: Usage;
191
+ messages?: Array<{ delivery_id?: string; message_id?: string }>;
185
192
  };
186
193
  }
187
194
 
@@ -242,9 +249,9 @@ export interface AgentSessionFactory {
242
249
  export interface RootConversationEndpoint {
243
250
  /** Queue one typed coordinator message into the root conversation. */
244
251
  queueCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
245
- hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string, deliveryId?: string): boolean;
246
- /** Report whether automatic delivery can start a new root turn without racing an active wait. */
252
+ /** Whether a coordinator message can start immediately instead of joining Pi's steer queue. */
247
253
  isIdle(): boolean;
254
+ hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string, deliveryId?: string): boolean;
248
255
  }
249
256
 
250
257
  /** Stores root-owned agent identity, launch contract, availability, and latest activity. */