@ian-pascoe/pi-minimal-subagents 0.2.0 → 0.2.2

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,22 +141,24 @@ 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
159
- unbounded checkpoint growth. The pure Delivery Ledger state machine retains at
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 and sends every unclaimed Coordination Message and terminal
159
+ result as a Pi steer, including while the recipient is active. Destination-session
160
+ Delivery Evidence settles and compacts ledger items, preventing duplicate delivery
161
+ and unbounded checkpoint growth. The pure Delivery Ledger state machine retains at
160
162
  most 20 pending wait-only terminal results per source agent; Coordination
161
163
  Messages are not removed by that terminal-retention limit. Delivered messages
162
164
  include stable delivery, source-agent, and source-turn identities in persisted
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-minimal-subagents",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "private": false,
5
5
  "description": "Persistent nested subagents with bounded delegation for Pi",
6
6
  "keywords": [
@@ -36,18 +36,7 @@ export function buildEligibleModelIds(input: {
36
36
  const source = scopeConfigured
37
37
  ? input.scopedModels.map((entry) => entry.model)
38
38
  : input.availableModels;
39
- const seen = new Set<string>();
40
- const result: string[] = [];
41
-
42
- for (const model of source) {
43
- const canonicalId = `${model.provider}/${model.id}`;
44
- if (!seen.has(canonicalId)) {
45
- seen.add(canonicalId);
46
- result.push(canonicalId);
47
- }
48
- }
49
-
50
- return result;
39
+ return [...new Set(source.map(({ provider, id }) => `${provider}/${id}`))];
51
40
  }
52
41
 
53
42
  /** Supplies inherited tools, the ancestor ceiling, and runtime availability for exact tool resolution. */
@@ -51,8 +51,8 @@ interface MinimalSubagentsSettingsDocument {
51
51
  }
52
52
 
53
53
  interface MinimalSubagentsConfigInput {
54
- globalSettings: MinimalSubagentsSettingsDocument;
55
- projectSettings: MinimalSubagentsSettingsDocument;
54
+ globalSettings: MinimalSubagentsSettingsDocumentInput;
55
+ projectSettings: MinimalSubagentsSettingsDocumentInput;
56
56
  eligibleModelIds: readonly string[];
57
57
  }
58
58
 
@@ -126,19 +126,8 @@ function parseModelRolesWireValue(value: JsonValue): ModelRolesWireValue {
126
126
  };
127
127
  }
128
128
 
129
- function parsePiSettingsDocument(
130
- settings: MinimalSubagentsSettingsDocumentInput,
131
- ): MinimalSubagentsSettingsDocument {
132
- if (!Value.Check(SettingsDocumentSchema, settings)) return {};
133
- const parsed: MinimalSubagentsSettingsDocument = {};
134
- if (settings.minimalSubagents !== undefined) {
135
- parsed.minimalSubagents = settings.minimalSubagents;
136
- }
137
- return parsed;
138
- }
139
-
140
129
  function readMinimalSubagentsSettings(
141
- settings: MinimalSubagentsSettingsDocument,
130
+ settings: MinimalSubagentsSettingsDocumentInput,
142
131
  scope: SettingsScope,
143
132
  warnings: string[],
144
133
  ): ParsedMinimalSubagentsSettings {
@@ -359,8 +348,8 @@ export function resolveMinimalSubagentsSettings(
359
348
  eligibleModelIds: readonly string[],
360
349
  ): ResolvedMinimalSubagentsConfig {
361
350
  return resolveMinimalSubagentsConfig({
362
- globalSettings: parsePiSettingsDocument(settings.getGlobalSettings()),
363
- projectSettings: parsePiSettingsDocument(settings.getProjectSettings()),
351
+ globalSettings: settings.getGlobalSettings(),
352
+ projectSettings: settings.getProjectSettings(),
364
353
  eligibleModelIds,
365
354
  });
366
355
  }
@@ -1,5 +1,4 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
2
  import { assembleImportedContext, contextContainsImages } from "./minimal-subagents-context.js";
4
3
  import {
5
4
  canAgentContractSpawn,
@@ -54,6 +53,7 @@ import type {
54
53
  StatusResult,
55
54
  TurnId,
56
55
  TurnResult,
56
+ WaitMessageResult,
57
57
  WaitResult,
58
58
  } from "./minimal-subagents-types.js";
59
59
 
@@ -86,11 +86,6 @@ interface PendingParentMessage {
86
86
  cancelGrace?: () => void;
87
87
  }
88
88
 
89
- interface CancelableWait {
90
- promise: Promise<void>;
91
- cancel: () => void;
92
- }
93
-
94
89
  function agentDeliveryKey(agentId: string, turnId: string): string {
95
90
  return `${agentId}\u0000${turnId}`;
96
91
  }
@@ -110,8 +105,9 @@ function terminalTurnResult(
110
105
  };
111
106
  }
112
107
 
113
- function terminalWaitResult(result: TurnResult): WaitResult {
114
- 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) };
115
111
  }
116
112
 
117
113
  /** One root-owned coordinator for persistent nested Pi child sessions. */
@@ -119,18 +115,15 @@ export class MinimalSubagentsCoordinator {
119
115
  private readonly agents = new Map<string, PersistedAgent>();
120
116
  private readonly runtimes = new Map<string, ChildAgentRuntime>();
121
117
  private readonly runtimeInitializations = new Map<string, Promise<ChildAgentRuntime>>();
122
- private readonly importedMessages = new Map<string, AgentMessage[]>();
123
118
  private readonly tombstones = new Set<string>();
124
119
  private readonly pendingAgentIds = new Set<string>();
125
120
  private deliveryLedger: DeliveryLedger = createDeliveryLedger();
126
121
  private readonly waiters = new Map<string, Set<TurnWaiter>>();
127
122
  private readonly pendingParentMessages = new Map<string, PendingParentMessage[]>();
128
123
  private readonly recipientQueues = new Map<string, Promise<unknown>>();
129
- private readonly recipientIdleWaiters = new Map<string, Set<() => void>>();
130
124
  private readonly automaticDeliveryKeys = new Set<string>();
131
125
  private readonly automaticCoordinationDeliveryIds = new Set<string>();
132
126
  private readonly waitHandedDeliveryIds = new Set<string>();
133
- private readonly automaticDeliveryClaimWaiters = new Map<string, Set<() => void>>();
134
127
  private readonly backgroundOperations = new Set<Promise<void>>();
135
128
  private acceptingOperations = true;
136
129
  private lifecycleEpoch = 0;
@@ -252,7 +245,6 @@ export class MinimalSubagentsCoordinator {
252
245
  agent.session_id = identity.sessionId;
253
246
  agent.session_leaf_id = identity.sessionLeafId;
254
247
  this.agents.set(agentId, agent);
255
- this.importedMessages.set(agentId, imported.messages);
256
248
  this.dependencies.registry.append(
257
249
  createRegistryEvent(this.dependencies.registry.rootSessionId, "agent-created", { agent }),
258
250
  );
@@ -366,15 +358,16 @@ export class MinimalSubagentsCoordinator {
366
358
  new Error(`Minimal subagents duplicate wait: ${callerId} is already waiting for ${turnId}`),
367
359
  );
368
360
  }
369
- const pendingMessage = this.claimPendingParentMessage(callerId, agentId, turnId);
370
- if (pendingMessage) return Promise.resolve(pendingMessage);
371
361
  const retainedResult =
372
362
  findTerminalDelivery(this.deliveryLedger, agentId, turnId)?.result ??
373
363
  (agent.latest_result?.turn_id === turnId ? agent.latest_result : undefined);
374
364
  if (retainedResult) {
365
+ const messages = this.drainPendingParentMessages(callerId, agentId, turnId);
375
366
  this.claimTerminalDelivery(callerId, retainedResult);
376
- return Promise.resolve(terminalWaitResult(retainedResult));
367
+ return Promise.resolve(terminalWaitResult(retainedResult, messages));
377
368
  }
369
+ const pendingMessage = this.claimPendingParentMessage(callerId, agentId, turnId);
370
+ if (pendingMessage) return Promise.resolve(pendingMessage);
378
371
  if (agent.active_turn_id !== turnId) {
379
372
  return Promise.reject(
380
373
  new Error(`Minimal subagents wait: turn ${turnId} is no longer retained for ${agentId}`),
@@ -505,7 +498,6 @@ export class MinimalSubagentsCoordinator {
505
498
  result.trashed_session_files.push(agent.session_file);
506
499
  }
507
500
  this.agents.delete(agent.agent_id);
508
- this.importedMessages.delete(agent.agent_id);
509
501
  this.pruneDeliveryStateForDeletedAgent(agent.agent_id);
510
502
  this.pruneRecentMessageProjectionsForDeletedAgent(agent.agent_id);
511
503
  this.tombstones.add(agent.agent_id);
@@ -520,10 +512,7 @@ export class MinimalSubagentsCoordinator {
520
512
  failedAncestors.add(agent.agent_id);
521
513
  if (runtime && agent.session_file) {
522
514
  try {
523
- this.runtimes.set(
524
- agent.agent_id,
525
- await this.dependencies.sessions.restoreRuntime(agent),
526
- );
515
+ this.runtimes.set(agent.agent_id, await this.dependencies.sessions.openRuntime(agent));
527
516
  } catch (restoreError) {
528
517
  agent.availability = "unavailable";
529
518
  agent.unavailable_reason = `Deletion recovery failed: ${
@@ -550,7 +539,6 @@ export class MinimalSubagentsCoordinator {
550
539
  this.agents.clear();
551
540
  this.deliveryLedger = createDeliveryLedger();
552
541
  this.pendingParentMessages.clear();
553
- this.releaseAllRecipientIdleWaiters();
554
542
  this.recipientQueues.clear();
555
543
  this.backgroundOperations.clear();
556
544
  await Promise.allSettled(
@@ -559,19 +547,16 @@ export class MinimalSubagentsCoordinator {
559
547
  for (const runtime of abandonedRuntimes) runtime.dispose();
560
548
  this.runtimes.clear();
561
549
  this.runtimeInitializations.clear();
562
- this.importedMessages.clear();
563
550
  this.pendingAgentIds.clear();
564
551
  this.tombstones.clear();
565
552
  this.deliveryLedger = createDeliveryLedger();
566
553
  this.waiters.clear();
567
554
  this.pendingParentMessages.clear();
568
555
  this.waitHandedDeliveryIds.clear();
569
- this.releaseAllRecipientIdleWaiters();
570
556
  this.recipientQueues.clear();
571
557
  this.backgroundOperations.clear();
572
558
  this.automaticDeliveryKeys.clear();
573
559
  this.automaticCoordinationDeliveryIds.clear();
574
- this.automaticDeliveryClaimWaiters.clear();
575
560
  this.deliveryLedger = createDeliveryLedger({
576
561
  deliveries: snapshot.deliveries,
577
562
  coordination_deliveries: snapshot.coordination_deliveries,
@@ -623,7 +608,7 @@ export class MinimalSubagentsCoordinator {
623
608
  });
624
609
  continue;
625
610
  }
626
- const runtime = await this.dependencies.sessions.restoreRuntime(agent);
611
+ const runtime = await this.dependencies.sessions.openRuntime(agent);
627
612
  if (restoreEpoch !== this.lifecycleEpoch) {
628
613
  runtime.dispose();
629
614
  return;
@@ -728,13 +713,6 @@ export class MinimalSubagentsCoordinator {
728
713
  await Promise.allSettled(scheduled);
729
714
  }
730
715
 
731
- /** Release ordered automatic deliveries after one recipient conversation becomes idle. */
732
- markRecipientIdle(agentId: string): void {
733
- const waiters = this.recipientIdleWaiters.get(agentId);
734
- this.recipientIdleWaiters.delete(agentId);
735
- for (const resolve of waiters ?? []) resolve();
736
- }
737
-
738
716
  /** Clone complete child leaves for root fork ownership without ever sharing source session paths. */
739
717
  async prepareFork(sourceRootSessionFile: string): Promise<ForkSnapshot> {
740
718
  const activeRootChildren = this.childrenOf("root");
@@ -793,7 +771,6 @@ export class MinimalSubagentsCoordinator {
793
771
  shutdown(): Promise<void> {
794
772
  if (this.shutdownPromise) return this.shutdownPromise;
795
773
  this.acceptingOperations = false;
796
- this.releaseAllRecipientIdleWaiters();
797
774
  this.shutdownPromise = this.finishShutdown();
798
775
  return this.shutdownPromise;
799
776
  }
@@ -902,19 +879,14 @@ export class MinimalSubagentsCoordinator {
902
879
  new Error(agent.clone_error ?? `No persistent session exists for ${agent.agent_id}`),
903
880
  );
904
881
  }
905
- const importedMessages = this.importedMessages.get(agent.agent_id);
906
- const initialization = (
907
- importedMessages
908
- ? this.dependencies.sessions.createRuntime({ agent, importedMessages })
909
- : this.dependencies.sessions.restoreRuntime(agent)
910
- )
882
+ const initialization = this.dependencies.sessions
883
+ .openRuntime(agent)
911
884
  .then((runtime) => {
912
885
  if (this.agents.get(agent.agent_id) !== agent) {
913
886
  runtime.dispose();
914
887
  throw new Error(`Minimal subagents runtime replaced while opening ${agent.agent_id}`);
915
888
  }
916
889
  this.runtimes.set(agent.agent_id, runtime);
917
- this.importedMessages.delete(agent.agent_id);
918
890
  return runtime;
919
891
  })
920
892
  .finally(() => {
@@ -1012,7 +984,6 @@ export class MinimalSubagentsCoordinator {
1012
984
  });
1013
985
  }
1014
986
  if (result.status !== "completed") this.removeSettledEmptyTurnClaim(agent.agent_id, turnId);
1015
- this.markRecipientIdle(agent.agent_id);
1016
987
  }
1017
988
 
1018
989
  private async deliverAutomaticResult(
@@ -1037,15 +1008,6 @@ export class MinimalSubagentsCoordinator {
1037
1008
  this.settleDelivery(delivery);
1038
1009
  return;
1039
1010
  }
1040
- while (!this.isRecipientIdle(delivery.destination_agent_id)) {
1041
- const idleWait = this.createRecipientIdleWait(delivery.destination_agent_id);
1042
- const claimWait = this.createAutomaticDeliveryClaimWait(deliveryKey);
1043
- await Promise.race([idleWait.promise, claimWait.promise]);
1044
- idleWait.cancel();
1045
- claimWait.cancel();
1046
- if (!this.acceptingOperations || this.shouldStopAutomaticTerminalDelivery(delivery))
1047
- return;
1048
- }
1049
1011
  if (!this.acceptingOperations || this.shouldStopAutomaticTerminalDelivery(delivery)) return;
1050
1012
  if (this.hasDeliveryEvidence(delivery)) {
1051
1013
  this.settleDelivery(delivery);
@@ -1064,11 +1026,8 @@ export class MinimalSubagentsCoordinator {
1064
1026
  usage: result.usage,
1065
1027
  },
1066
1028
  };
1067
- await this.deliverToRecipient(
1068
- delivery.destination_agent_id,
1069
- message,
1070
- () => this.isTerminalDeliveryCurrent(delivery),
1071
- true,
1029
+ await this.deliverToRecipient(delivery.destination_agent_id, message, () =>
1030
+ this.isTerminalDeliveryCurrent(delivery),
1072
1031
  );
1073
1032
  });
1074
1033
  } catch (error) {
@@ -1276,7 +1235,6 @@ export class MinimalSubagentsCoordinator {
1276
1235
  targetId: string,
1277
1236
  message: CoordinatorMessage,
1278
1237
  isCurrentDelivery: () => boolean = () => true,
1279
- requireIdleRecipient = false,
1280
1238
  ): Promise<void> {
1281
1239
  if (!isCurrentDelivery()) {
1282
1240
  throw new Error("Minimal subagents delivery abandoned after session branch change");
@@ -1294,9 +1252,6 @@ export class MinimalSubagentsCoordinator {
1294
1252
  throw new Error("Minimal subagents delivery abandoned after session branch change");
1295
1253
  }
1296
1254
  const visibleMessage = addCoordinatorMessageEnvelope(message);
1297
- if (requireIdleRecipient && (target.active_turn_id || runtime.isRunning)) {
1298
- throw new Error(`Minimal subagents automatic delivery recipient became active: ${targetId}`);
1299
- }
1300
1255
  if (target.active_turn_id || runtime.isRunning) {
1301
1256
  await runtime.queueCoordinatorMessage(visibleMessage);
1302
1257
  return;
@@ -1380,8 +1335,6 @@ export class MinimalSubagentsCoordinator {
1380
1335
  private applyDeliveryLedgerTransition(transition: DeliveryLedgerTransition): void {
1381
1336
  this.deliveryLedger = transition.ledger;
1382
1337
  for (const delivery of transition.prunedTerminalDeliveries) {
1383
- const key = agentDeliveryKey(delivery.source_agent_id, delivery.source_turn_id);
1384
- this.releaseAutomaticDeliveryClaimWaiters(key);
1385
1338
  this.dependencies.registry.append(
1386
1339
  createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pruned", {
1387
1340
  source_agent_id: delivery.source_agent_id,
@@ -1517,16 +1470,8 @@ export class MinimalSubagentsCoordinator {
1517
1470
  }
1518
1471
 
1519
1472
  private pruneDeliveryStateForDeletedAgent(agentId: string): void {
1520
- const previousTerminalDeliveries = this.deliveryLedger.terminalDeliveries;
1521
1473
  const previousCoordinationDeliveries = this.deliveryLedger.coordinationDeliveries;
1522
1474
  this.deliveryLedger = pruneDeliveryLedgerAgents(this.deliveryLedger, [agentId]).ledger;
1523
- for (const delivery of previousTerminalDeliveries) {
1524
- if (!this.isTerminalDeliveryCurrent(delivery)) {
1525
- this.releaseAutomaticDeliveryClaimWaiters(
1526
- agentDeliveryKey(delivery.source_agent_id, delivery.source_turn_id),
1527
- );
1528
- }
1529
- }
1530
1475
  for (const delivery of previousCoordinationDeliveries) {
1531
1476
  if (!this.isCoordinationDeliveryCurrent(delivery)) {
1532
1477
  this.waitHandedDeliveryIds.delete(delivery.delivery_id);
@@ -1686,7 +1631,6 @@ export class MinimalSubagentsCoordinator {
1686
1631
  (candidate) => candidate.callerId === destinationAgentId,
1687
1632
  );
1688
1633
  if (!waiter) return false;
1689
- this.claimDeliveryTurn(message.details.source_agent_id, message.details.source_turn_id);
1690
1634
  this.deliveryLedger = setCoordinationDeliveryPath(
1691
1635
  this.deliveryLedger,
1692
1636
  delivery.delivery_id,
@@ -1711,7 +1655,7 @@ export class MinimalSubagentsCoordinator {
1711
1655
  callerId: string,
1712
1656
  sourceAgentId: string,
1713
1657
  sourceTurnId: string,
1714
- ): WaitResult | undefined {
1658
+ ): WaitMessageResult | undefined {
1715
1659
  const key = agentDeliveryKey(sourceAgentId, sourceTurnId);
1716
1660
  const pendingMessages = this.pendingParentMessages.get(key);
1717
1661
  const index = pendingMessages?.findIndex(
@@ -1728,7 +1672,6 @@ export class MinimalSubagentsCoordinator {
1728
1672
  )
1729
1673
  .sort((left, right) => left.sequence - right.sequence)[0];
1730
1674
  if (!retained) return undefined;
1731
- this.claimDeliveryTurn(sourceAgentId, sourceTurnId);
1732
1675
  this.deliveryLedger = setCoordinationDeliveryPath(
1733
1676
  this.deliveryLedger,
1734
1677
  retained.delivery_id,
@@ -1748,7 +1691,6 @@ export class MinimalSubagentsCoordinator {
1748
1691
  }
1749
1692
  const pending = pendingMessages[index];
1750
1693
  if (!pending) return undefined;
1751
- this.claimDeliveryTurn(sourceAgentId, sourceTurnId);
1752
1694
  const delivery = findCoordinationDelivery(this.deliveryLedger, pending.deliveryId);
1753
1695
  if (delivery) {
1754
1696
  this.deliveryLedger = setCoordinationDeliveryPath(
@@ -1760,10 +1702,6 @@ export class MinimalSubagentsCoordinator {
1760
1702
  if (currentDelivery) this.persistCoordinationDeliveryState(currentDelivery);
1761
1703
  this.waitHandedDeliveryIds.add(delivery.delivery_id);
1762
1704
  }
1763
- const sourceResult = this.agents.get(sourceAgentId)?.latest_result;
1764
- if (sourceResult?.turn_id === sourceTurnId) {
1765
- this.setTerminalDeliveryPathToWait(callerId, sourceResult);
1766
- }
1767
1705
  pending.claimed = true;
1768
1706
  pending.cancelGrace?.();
1769
1707
  pending.releaseClaim();
@@ -1779,6 +1717,20 @@ export class MinimalSubagentsCoordinator {
1779
1717
  };
1780
1718
  }
1781
1719
 
1720
+ private drainPendingParentMessages(
1721
+ callerId: string,
1722
+ sourceAgentId: string,
1723
+ sourceTurnId: string,
1724
+ ): WaitMessageResult[] {
1725
+ const messages: WaitMessageResult[] = [];
1726
+ let message = this.claimPendingParentMessage(callerId, sourceAgentId, sourceTurnId);
1727
+ while (message) {
1728
+ messages.push(message);
1729
+ message = this.claimPendingParentMessage(callerId, sourceAgentId, sourceTurnId);
1730
+ }
1731
+ return messages;
1732
+ }
1733
+
1782
1734
  private queuePendingParentMessage(
1783
1735
  targetId: string,
1784
1736
  message: CoordinatorMessage,
@@ -1791,10 +1743,7 @@ export class MinimalSubagentsCoordinator {
1791
1743
  message.details.source_agent_id,
1792
1744
  message.details.source_turn_id,
1793
1745
  );
1794
- let releaseClaim!: () => void;
1795
- const claimPromise = new Promise<void>((resolve) => {
1796
- releaseClaim = resolve;
1797
- });
1746
+ const { promise: claimPromise, resolve: releaseClaim } = Promise.withResolvers<void>();
1798
1747
  const pending: PendingParentMessage = {
1799
1748
  deliveryId: delivery.delivery_id,
1800
1749
  message,
@@ -1837,20 +1786,11 @@ export class MinimalSubagentsCoordinator {
1837
1786
  pending.cancelGrace?.();
1838
1787
  pending.cancelGrace = undefined;
1839
1788
  if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
1840
- while (!this.isRecipientIdle(targetId)) {
1841
- const idleWait = this.createRecipientIdleWait(targetId);
1842
- await Promise.race([pending.claimPromise, idleWait.promise]);
1843
- idleWait.cancel();
1844
- if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
1845
- }
1846
1789
  if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
1847
1790
  this.removePendingParentMessage(key, pending);
1848
1791
  this.waitHandedDeliveryIds.add(delivery.delivery_id);
1849
- await this.deliverToRecipient(
1850
- targetId,
1851
- message,
1852
- () => this.isCoordinationDeliveryCurrent(delivery),
1853
- true,
1792
+ await this.deliverToRecipient(targetId, message, () =>
1793
+ this.isCoordinationDeliveryCurrent(delivery),
1854
1794
  );
1855
1795
  });
1856
1796
  void operation.catch((cause) => {
@@ -1890,14 +1830,12 @@ export class MinimalSubagentsCoordinator {
1890
1830
  }
1891
1831
 
1892
1832
  private setTerminalDeliveryPathToWait(callerId: string, result: TurnResult): void {
1893
- const key = agentDeliveryKey(result.agent_id, result.turn_id);
1894
1833
  const delivery = findTerminalDelivery(this.deliveryLedger, result.agent_id, result.turn_id);
1895
1834
  if (!delivery || delivery.destination_agent_id !== callerId || delivery.path === "wait") return;
1896
1835
  this.applyDeliveryLedgerTransition(
1897
1836
  setTerminalDeliveryPath(this.deliveryLedger, result.agent_id, result.turn_id, "wait"),
1898
1837
  );
1899
1838
  const retained = findTerminalDelivery(this.deliveryLedger, result.agent_id, result.turn_id);
1900
- this.releaseAutomaticDeliveryClaimWaiters(key);
1901
1839
  if (!retained) return;
1902
1840
  this.dependencies.registry.append(
1903
1841
  createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pending", {
@@ -1906,63 +1844,6 @@ export class MinimalSubagentsCoordinator {
1906
1844
  );
1907
1845
  }
1908
1846
 
1909
- private isRecipientIdle(agentId: string): boolean {
1910
- if (agentId === "root") return this.dependencies.root.isIdle();
1911
- const agent = this.agents.get(agentId);
1912
- if (!agent || agent.active_turn_id) return false;
1913
- return !this.runtimes.get(agentId)?.isRunning;
1914
- }
1915
-
1916
- private createRecipientIdleWait(agentId: string): CancelableWait {
1917
- let release!: () => void;
1918
- const promise = new Promise<void>((resolve) => {
1919
- release = resolve;
1920
- });
1921
- const waiters = this.recipientIdleWaiters.get(agentId) ?? new Set();
1922
- waiters.add(release);
1923
- this.recipientIdleWaiters.set(agentId, waiters);
1924
- return {
1925
- promise,
1926
- cancel: () => {
1927
- const currentWaiters = this.recipientIdleWaiters.get(agentId);
1928
- currentWaiters?.delete(release);
1929
- if (currentWaiters?.size === 0) this.recipientIdleWaiters.delete(agentId);
1930
- },
1931
- };
1932
- }
1933
-
1934
- private createAutomaticDeliveryClaimWait(deliveryKey: string): CancelableWait {
1935
- let release!: () => void;
1936
- const promise = new Promise<void>((resolve) => {
1937
- release = resolve;
1938
- });
1939
- const waiters = this.automaticDeliveryClaimWaiters.get(deliveryKey) ?? new Set();
1940
- waiters.add(release);
1941
- this.automaticDeliveryClaimWaiters.set(deliveryKey, waiters);
1942
- return {
1943
- promise,
1944
- cancel: () => {
1945
- const currentWaiters = this.automaticDeliveryClaimWaiters.get(deliveryKey);
1946
- currentWaiters?.delete(release);
1947
- if (currentWaiters?.size === 0) this.automaticDeliveryClaimWaiters.delete(deliveryKey);
1948
- },
1949
- };
1950
- }
1951
-
1952
- private releaseAutomaticDeliveryClaimWaiters(deliveryKey: string): void {
1953
- const waiters = this.automaticDeliveryClaimWaiters.get(deliveryKey);
1954
- this.automaticDeliveryClaimWaiters.delete(deliveryKey);
1955
- for (const resolve of waiters ?? []) resolve();
1956
- }
1957
-
1958
- private releaseAllRecipientIdleWaiters(): void {
1959
- const allWaiters = [...this.recipientIdleWaiters.values()];
1960
- this.recipientIdleWaiters.clear();
1961
- for (const waiters of allWaiters) {
1962
- for (const resolve of waiters) resolve();
1963
- }
1964
- }
1965
-
1966
1847
  private async cancelDuringShutdown(agentId: string): Promise<void> {
1967
1848
  const target = this.agents.get(agentId);
1968
1849
  if (!target) return;
@@ -58,23 +58,13 @@ function deliveryTurnKey(sourceAgentId: string, sourceTurnId: string): string {
58
58
  return `${sourceAgentId}\u0000${sourceTurnId}`;
59
59
  }
60
60
 
61
- function cloneTerminalDelivery(delivery: PersistedDelivery): PersistedDelivery {
62
- return structuredClone(delivery);
63
- }
64
-
65
- function cloneCoordinationDelivery(
66
- delivery: PersistedCoordinationDelivery,
67
- ): PersistedCoordinationDelivery {
68
- return structuredClone(delivery);
69
- }
70
-
71
61
  function createTransition(
72
62
  ledger: DeliveryLedger,
73
63
  prunedTerminalDeliveries: readonly PersistedDelivery[] = [],
74
64
  ): DeliveryLedgerTransition {
75
65
  return {
76
66
  ledger,
77
- prunedTerminalDeliveries: prunedTerminalDeliveries.map(cloneTerminalDelivery),
67
+ prunedTerminalDeliveries: prunedTerminalDeliveries.map((delivery) => structuredClone(delivery)),
78
68
  };
79
69
  }
80
70
 
@@ -134,7 +124,7 @@ export function createDeliveryLedger(
134
124
  ): DeliveryLedger {
135
125
  const coordinationDeliveries = (snapshot.coordination_deliveries ?? [])
136
126
  .filter((delivery) => !delivery.settled)
137
- .map(cloneCoordinationDelivery);
127
+ .map((delivery) => structuredClone(delivery));
138
128
  let nextSequence = Math.max(
139
129
  1,
140
130
  snapshot.next_delivery_sequence ?? 1,
@@ -146,7 +136,7 @@ export function createDeliveryLedger(
146
136
  const terminalDeliveries = (snapshot.deliveries ?? [])
147
137
  .filter((delivery) => !delivery.settled)
148
138
  .map((delivery) => {
149
- const restored = cloneTerminalDelivery(delivery);
139
+ const restored = structuredClone(delivery);
150
140
  if (restored.sequence === undefined) restored.sequence = nextSequence++;
151
141
  return restored;
152
142
  });
@@ -161,8 +151,10 @@ export function createDeliveryLedger(
161
151
  /** Serialize pending Delivery Ledger state without exposing mutable internal references. */
162
152
  export function deliveryLedgerSnapshot(ledger: DeliveryLedger): DeliveryLedgerSnapshot {
163
153
  return {
164
- deliveries: ledger.terminalDeliveries.map(cloneTerminalDelivery),
165
- coordination_deliveries: ledger.coordinationDeliveries.map(cloneCoordinationDelivery),
154
+ deliveries: ledger.terminalDeliveries.map((delivery) => structuredClone(delivery)),
155
+ coordination_deliveries: ledger.coordinationDeliveries.map((delivery) =>
156
+ structuredClone(delivery),
157
+ ),
166
158
  wait_claimed_turns: [...ledger.waitClaimedTurns],
167
159
  next_delivery_sequence: ledger.nextSequence,
168
160
  };
@@ -194,7 +186,7 @@ export function addTerminalDelivery(
194
186
  ],
195
187
  nextSequence: ledger.nextSequence + 1,
196
188
  });
197
- return { ...transition, delivery: cloneTerminalDelivery(delivery) };
189
+ return { ...transition, delivery: structuredClone(delivery) };
198
190
  }
199
191
 
200
192
  /** Replay or update one already-sequenced terminal delivery. */
@@ -202,7 +194,7 @@ export function upsertTerminalDelivery(
202
194
  ledger: DeliveryLedger,
203
195
  delivery: PersistedDelivery,
204
196
  ): DeliveryLedgerTransition {
205
- const restored = cloneTerminalDelivery(delivery);
197
+ const restored = structuredClone(delivery);
206
198
  const existing = ledger.terminalDeliveries.find(
207
199
  (current) =>
208
200
  deliveryTurnKey(current.source_agent_id, current.source_turn_id) ===
@@ -251,7 +243,7 @@ export function addCoordinationDelivery(
251
243
  ],
252
244
  nextSequence: ledger.nextSequence + 1,
253
245
  },
254
- delivery: cloneCoordinationDelivery(delivery),
246
+ delivery: structuredClone(delivery),
255
247
  prunedTerminalDeliveries: [],
256
248
  };
257
249
  }
@@ -267,7 +259,7 @@ export function upsertCoordinationDelivery(
267
259
  ...ledger.coordinationDeliveries.filter(
268
260
  (current) => current.delivery_id !== delivery.delivery_id,
269
261
  ),
270
- cloneCoordinationDelivery(delivery),
262
+ structuredClone(delivery),
271
263
  ],
272
264
  nextSequence: Math.max(ledger.nextSequence, delivery.sequence + 1),
273
265
  });
@@ -505,7 +497,7 @@ export function findTerminalDelivery(
505
497
  const delivery = ledger.terminalDeliveries.find(
506
498
  (candidate) => deliveryTurnKey(candidate.source_agent_id, candidate.source_turn_id) === key,
507
499
  );
508
- return delivery ? cloneTerminalDelivery(delivery) : undefined;
500
+ return delivery ? structuredClone(delivery) : undefined;
509
501
  }
510
502
 
511
503
  /** Return one pending Coordination Message by stable delivery identity. */
@@ -516,7 +508,7 @@ export function findCoordinationDelivery(
516
508
  const delivery = ledger.coordinationDeliveries.find(
517
509
  (candidate) => candidate.delivery_id === deliveryId,
518
510
  );
519
- return delivery ? cloneCoordinationDelivery(delivery) : undefined;
511
+ return delivery ? structuredClone(delivery) : undefined;
520
512
  }
521
513
 
522
514
  /** Report whether one source turn currently has a durable wait claim. */
@@ -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,
@@ -107,7 +106,6 @@ function createRootConversationEndpoint(
107
106
  sourceTurnId,
108
107
  deliveryId,
109
108
  ),
110
- isIdle: () => context.isIdle(),
111
109
  };
112
110
  }
113
111
 
@@ -331,7 +329,6 @@ export class MinimalSubagentsLifecycleController {
331
329
  this.pi.on("session_before_fork", (event, context) => this.prepareSessionFork(event, context));
332
330
  this.pi.on("session_tree", (event, context) => this.restoreSessionTree(event, context));
333
331
  this.pi.on("message_end", (event, context) => this.reconcileMessageDelivery(event, context));
334
- this.pi.on("agent_settled", (event, context) => this.releaseSettledRecipient(event, context));
335
332
  this.pi.on("session_shutdown", (event, context) => this.shutdownSession(event, context));
336
333
  }
337
334
 
@@ -554,12 +551,6 @@ export class MinimalSubagentsLifecycleController {
554
551
  }
555
552
  }
556
553
 
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
554
  private async shutdownSession(
564
555
  event: SessionShutdownEvent,
565
556
  context: ExtensionContext,
@@ -740,12 +740,6 @@ function validateRegistrySnapshot(
740
740
  }
741
741
  const identityError = validateDeliveryIdentity(delivery, version);
742
742
  if (identityError) return invalidRegistrySnapshot("invalid-delivery-identity", identityError);
743
- if (!isOwnedTurnId(delivery.source_agent_id, delivery.source_turn_id)) {
744
- return invalidRegistrySnapshot(
745
- "invalid-delivery-identity",
746
- "terminal source_turn_id must belong to source_agent_id",
747
- );
748
- }
749
743
  const source = agentsById.get(delivery.source_agent_id);
750
744
  if (!source) {
751
745
  return invalidRegistrySnapshot(
@@ -763,17 +757,6 @@ function validateRegistrySnapshot(
763
757
  for (const delivery of coordinationDeliveries) {
764
758
  const identityError = validateCoordinationIdentity(delivery);
765
759
  if (identityError) return invalidRegistrySnapshot("invalid-delivery-identity", identityError);
766
- if (
767
- !isOwnedTurnId(
768
- delivery.message.details.source_agent_id,
769
- delivery.message.details.source_turn_id,
770
- )
771
- ) {
772
- return invalidRegistrySnapshot(
773
- "invalid-delivery-identity",
774
- "Coordination Message source_turn_id must belong to source_agent_id",
775
- );
776
- }
777
760
  if (
778
761
  !areAdjacentAgents(
779
762
  delivery.message.details.source_agent_id,
@@ -1228,13 +1211,7 @@ export function replayRegistryEntries(
1228
1211
  }
1229
1212
  });
1230
1213
 
1231
- let checkpointIndex = -1;
1232
- for (let index = parsedEvents.length - 1; index >= 0; index--) {
1233
- if (parsedEvents[index]?.event.event === "checkpoint") {
1234
- checkpointIndex = index;
1235
- break;
1236
- }
1237
- }
1214
+ const checkpointIndex = parsedEvents.findLastIndex(({ event }) => event.event === "checkpoint");
1238
1215
  const checkpointEvent = checkpointIndex >= 0 ? parsedEvents[checkpointIndex]?.event : undefined;
1239
1216
  const checkpoint = checkpointEvent?.event === "checkpoint" ? checkpointEvent.snapshot : undefined;
1240
1217
  const agents = new Map(
@@ -3,7 +3,7 @@ import type {
3
3
  MessageRenderer,
4
4
  ToolDefinition,
5
5
  } from "@earendil-works/pi-coding-agent";
6
- import { Type, type Static, type TSchema } from "typebox";
6
+ import { Type, type Static } from "typebox";
7
7
  import { Value } from "typebox/value";
8
8
 
9
9
  /** Names of the six coordinator tools with custom transcript renderers. */
@@ -107,16 +107,6 @@ const ManagementCallArgumentsSchema = Type.Object({
107
107
  recursive: Type.Optional(Type.Boolean()),
108
108
  });
109
109
 
110
- /** Tool-call contracts keep each coordinator name correlated with its own arguments. */
111
- export const coordinatorToolCallSchemas = {
112
- subagent: SpawnCallArgumentsSchema,
113
- agent_message: MessageCallArgumentsSchema,
114
- subagent_wait: WaitCallArgumentsSchema,
115
- subagent_status: StatusCallArgumentsSchema,
116
- subagent_cancel: ManagementCallArgumentsSchema,
117
- subagent_delete: ManagementCallArgumentsSchema,
118
- } as const satisfies { readonly [Name in CoordinatorToolName]: TSchema };
119
-
120
110
  const SpawnDetailsSchema = Type.Object({
121
111
  agent_id: Type.String(),
122
112
  turn_id: Type.String(),
@@ -142,6 +132,7 @@ const WaitMessageDetailsSchema = Type.Object({
142
132
  agent_id: Type.String(),
143
133
  turn_id: Type.String(),
144
134
  message_id: Type.String(),
135
+ delivery_id: Type.Optional(Type.String()),
145
136
  message: Type.String(),
146
137
  elapsed_ms: Type.Optional(Type.Number()),
147
138
  usage: Type.Optional(RenderUsageSchema),
@@ -155,6 +146,7 @@ const WaitTurnDetailsSchema = Type.Object({
155
146
  error: Type.Optional(Type.String()),
156
147
  elapsed_ms: Type.Optional(Type.Number()),
157
148
  usage: Type.Optional(RenderUsageSchema),
149
+ messages: Type.Optional(Type.Array(WaitMessageDetailsSchema)),
158
150
  });
159
151
  const StatusDetailsSchema = Type.Union([
160
152
  Type.Object({
@@ -183,15 +175,11 @@ const DeleteDetailsSchema = Type.Object({
183
175
  ),
184
176
  });
185
177
 
186
- /** Result-detail contracts keep current and tolerated legacy transcript shapes explicit. */
187
- export const coordinatorToolResultSchemas = {
188
- subagent: SpawnDetailsSchema,
189
- agent_message: Type.Union([CurrentMessageDetailsSchema, LegacyMessageDetailsSchema]),
190
- subagent_wait: Type.Union([WaitMessageDetailsSchema, WaitTurnDetailsSchema]),
191
- subagent_status: StatusDetailsSchema,
192
- subagent_cancel: CancelDetailsSchema,
193
- subagent_delete: DeleteDetailsSchema,
194
- } as const satisfies { readonly [Name in CoordinatorToolName]: TSchema };
178
+ const MessageRenderDetailsSchema = Type.Union([
179
+ CurrentMessageDetailsSchema,
180
+ LegacyMessageDetailsSchema,
181
+ ]);
182
+ const WaitRenderDetailsSchema = Type.Union([WaitMessageDetailsSchema, WaitTurnDetailsSchema]);
195
183
 
196
184
  export type SpawnCallArguments = Static<typeof SpawnCallArgumentsSchema>;
197
185
  export type MessageCallArguments = Static<typeof MessageCallArgumentsSchema>;
@@ -199,12 +187,8 @@ export type WaitCallArguments = Static<typeof WaitCallArgumentsSchema>;
199
187
  export type StatusCallArguments = Static<typeof StatusCallArgumentsSchema>;
200
188
  export type ManagementCallArguments = Static<typeof ManagementCallArgumentsSchema>;
201
189
  export type SpawnRenderDetails = Static<typeof SpawnDetailsSchema>;
202
- export type MessageRenderDetails = Static<
203
- typeof CurrentMessageDetailsSchema | typeof LegacyMessageDetailsSchema
204
- >;
205
- export type WaitRenderDetails = Static<
206
- typeof WaitMessageDetailsSchema | typeof WaitTurnDetailsSchema
207
- >;
190
+ export type MessageRenderDetails = Static<typeof MessageRenderDetailsSchema>;
191
+ export type WaitRenderDetails = Static<typeof WaitRenderDetailsSchema>;
208
192
  export type StatusRenderDetails = Static<typeof StatusDetailsSchema>;
209
193
  export type CancelRenderDetails = Static<typeof CancelDetailsSchema>;
210
194
  export type DeleteRenderDetails = Static<typeof DeleteDetailsSchema>;
@@ -263,13 +247,9 @@ export function parseCoordinatorToolResult(
263
247
  case "subagent":
264
248
  return Value.Check(SpawnDetailsSchema, details) ? { toolName, details } : undefined;
265
249
  case "agent_message":
266
- return Value.Check(coordinatorToolResultSchemas.agent_message, details)
267
- ? { toolName, details }
268
- : undefined;
250
+ return Value.Check(MessageRenderDetailsSchema, details) ? { toolName, details } : undefined;
269
251
  case "subagent_wait":
270
- return Value.Check(coordinatorToolResultSchemas.subagent_wait, details)
271
- ? { toolName, details }
272
- : undefined;
252
+ return Value.Check(WaitRenderDetailsSchema, details) ? { toolName, details } : undefined;
273
253
  case "subagent_status":
274
254
  return Value.Check(StatusDetailsSchema, details) ? { toolName, details } : undefined;
275
255
  case "subagent_cancel":
@@ -34,7 +34,6 @@ import {
34
34
  type RenderStatusAgent,
35
35
  type SpawnCallArguments,
36
36
  type SpawnRenderDetails,
37
- type StatusCallArguments,
38
37
  type StatusRenderDetails,
39
38
  type WaitCallArguments,
40
39
  type WaitRenderDetails,
@@ -259,59 +258,6 @@ function renderManagementToolCall(
259
258
  );
260
259
  }
261
260
 
262
- type CoordinatorToolCallRenderers = {
263
- readonly subagent: (args: SpawnCallArguments, theme: MinimalSubagentsRenderTheme) => Component;
264
- readonly agent_message: (
265
- args: MessageCallArguments,
266
- theme: MinimalSubagentsRenderTheme,
267
- ) => Component;
268
- readonly subagent_wait: (
269
- args: WaitCallArguments,
270
- theme: MinimalSubagentsRenderTheme,
271
- ) => Component;
272
- readonly subagent_status: (
273
- args: StatusCallArguments,
274
- theme: MinimalSubagentsRenderTheme,
275
- ) => Component;
276
- readonly subagent_cancel: (
277
- args: ManagementCallArguments,
278
- theme: MinimalSubagentsRenderTheme,
279
- ) => Component;
280
- readonly subagent_delete: (
281
- args: ManagementCallArguments,
282
- theme: MinimalSubagentsRenderTheme,
283
- ) => Component;
284
- };
285
-
286
- const COORDINATOR_TOOL_CALL_RENDERERS = {
287
- subagent: (args, theme) =>
288
- new Text(
289
- `${coordinatorToolCallTitle(theme, "Subagent")} ${theme.fg("accent", args.agent_id ?? "generated")}${coordinatorToolCallPreview(theme, args.task)}`,
290
- 0,
291
- 0,
292
- ),
293
- agent_message: (args, theme) =>
294
- new Text(
295
- `${coordinatorToolCallTitle(theme, "Message")} ${theme.fg("accent", args.agent_id ?? "parent")}${coordinatorToolCallPreview(theme, args.message)}`,
296
- 0,
297
- 0,
298
- ),
299
- subagent_wait: (args, theme) =>
300
- new Text(
301
- `${coordinatorToolCallTitle(theme, "Wait")} ${theme.fg("accent", args.agent_id ?? "agent")}`,
302
- 0,
303
- 0,
304
- ),
305
- subagent_status: (args, theme) =>
306
- new Text(
307
- `${coordinatorToolCallTitle(theme, "Status")} ${theme.fg("accent", args.agent_id ?? "children")}`,
308
- 0,
309
- 0,
310
- ),
311
- subagent_cancel: (args, theme) => renderManagementToolCall("Cancel", args, theme),
312
- subagent_delete: (args, theme) => renderManagementToolCall("Delete", args, theme),
313
- } satisfies CoordinatorToolCallRenderers;
314
-
315
261
  function renderSpawnResult(
316
262
  details: SpawnRenderDetails,
317
263
  options: ToolRenderResultOptions,
@@ -395,9 +341,12 @@ function renderWaitResult(
395
341
  : details.status;
396
342
  const duration = formatSubagentDuration(details.elapsed_ms);
397
343
  const tokens = formatSubagentTokenCount(details.usage?.totalTokens);
398
- const metrics = [duration, tokens ? `${tokens} tokens` : undefined].filter(
399
- (metric): metric is string => metric !== undefined,
400
- );
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);
401
350
  const summary = renderSubagentSummary(theme, status, agentId, metrics);
402
351
  if (options.isPartial || !options.expanded) {
403
352
  return new Text(`${summary}${options.isPartial ? "" : collapsedExpansionHint(theme)}`, 0, 0);
@@ -410,6 +359,14 @@ function renderWaitResult(
410
359
  container.addChild(renderLabelValue(theme, "Message ID", details.message_id));
411
360
  return container;
412
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
+ }
413
370
  const output = details.output ?? "";
414
371
  if (status === "completed") {
415
372
  if (output.length > 0) {
@@ -660,51 +617,6 @@ function renderDeleteResult(
660
617
  return container;
661
618
  }
662
619
 
663
- type CoordinatorToolResultRenderers = {
664
- readonly subagent: (
665
- details: SpawnRenderDetails,
666
- options: ToolRenderResultOptions,
667
- theme: MinimalSubagentsRenderTheme,
668
- args: SpawnCallArguments,
669
- ) => Component;
670
- readonly agent_message: (
671
- details: MessageRenderDetails,
672
- options: ToolRenderResultOptions,
673
- theme: MinimalSubagentsRenderTheme,
674
- args: MessageCallArguments,
675
- ) => Component;
676
- readonly subagent_wait: (
677
- details: WaitRenderDetails,
678
- options: ToolRenderResultOptions,
679
- theme: MinimalSubagentsRenderTheme,
680
- args: WaitCallArguments,
681
- ) => Component;
682
- readonly subagent_status: (
683
- details: StatusRenderDetails,
684
- options: ToolRenderResultOptions,
685
- theme: MinimalSubagentsRenderTheme,
686
- ) => Component;
687
- readonly subagent_cancel: (
688
- details: CancelRenderDetails,
689
- options: ToolRenderResultOptions,
690
- theme: MinimalSubagentsRenderTheme,
691
- ) => Component;
692
- readonly subagent_delete: (
693
- details: DeleteRenderDetails,
694
- options: ToolRenderResultOptions,
695
- theme: MinimalSubagentsRenderTheme,
696
- ) => Component;
697
- };
698
-
699
- const COORDINATOR_TOOL_RESULT_RENDERERS = {
700
- subagent: renderSpawnResult,
701
- agent_message: renderMessageResult,
702
- subagent_wait: renderWaitResult,
703
- subagent_status: renderStatusResult,
704
- subagent_cancel: renderCancelResult,
705
- subagent_delete: renderDeleteResult,
706
- } satisfies CoordinatorToolResultRenderers;
707
-
708
620
  /** Render one of the six coordinator tool calls with a shared native Pi grammar. */
709
621
  export function renderCoordinatorToolCall(
710
622
  toolName: CoordinatorToolName,
@@ -715,17 +627,33 @@ export function renderCoordinatorToolCall(
715
627
  if (parsed === undefined) return new Text(coordinatorToolCallTitle(theme, toolName), 0, 0);
716
628
  switch (parsed.toolName) {
717
629
  case "subagent":
718
- return COORDINATOR_TOOL_CALL_RENDERERS.subagent(parsed.args, theme);
630
+ return new Text(
631
+ `${coordinatorToolCallTitle(theme, "Subagent")} ${theme.fg("accent", parsed.args.agent_id ?? "generated")}${coordinatorToolCallPreview(theme, parsed.args.task)}`,
632
+ 0,
633
+ 0,
634
+ );
719
635
  case "agent_message":
720
- return COORDINATOR_TOOL_CALL_RENDERERS.agent_message(parsed.args, theme);
636
+ return new Text(
637
+ `${coordinatorToolCallTitle(theme, "Message")} ${theme.fg("accent", parsed.args.agent_id ?? "parent")}${coordinatorToolCallPreview(theme, parsed.args.message)}`,
638
+ 0,
639
+ 0,
640
+ );
721
641
  case "subagent_wait":
722
- return COORDINATOR_TOOL_CALL_RENDERERS.subagent_wait(parsed.args, theme);
642
+ return new Text(
643
+ `${coordinatorToolCallTitle(theme, "Wait")} ${theme.fg("accent", parsed.args.agent_id ?? "agent")}`,
644
+ 0,
645
+ 0,
646
+ );
723
647
  case "subagent_status":
724
- return COORDINATOR_TOOL_CALL_RENDERERS.subagent_status(parsed.args, theme);
648
+ return new Text(
649
+ `${coordinatorToolCallTitle(theme, "Status")} ${theme.fg("accent", parsed.args.agent_id ?? "children")}`,
650
+ 0,
651
+ 0,
652
+ );
725
653
  case "subagent_cancel":
726
- return COORDINATOR_TOOL_CALL_RENDERERS.subagent_cancel(parsed.args, theme);
654
+ return renderManagementToolCall("Cancel", parsed.args, theme);
727
655
  case "subagent_delete":
728
- return COORDINATOR_TOOL_CALL_RENDERERS.subagent_delete(parsed.args, theme);
656
+ return renderManagementToolCall("Delete", parsed.args, theme);
729
657
  }
730
658
  }
731
659
 
@@ -743,44 +671,32 @@ export function renderCoordinatorToolResult(
743
671
  const parsedCall = parseCoordinatorToolCall(toolName, args);
744
672
  switch (parsedResult.toolName) {
745
673
  case "subagent":
746
- return COORDINATOR_TOOL_RESULT_RENDERERS.subagent(
674
+ return renderSpawnResult(
747
675
  parsedResult.details,
748
676
  options,
749
677
  theme,
750
678
  parsedCall?.toolName === "subagent" ? parsedCall.args : {},
751
679
  );
752
680
  case "agent_message":
753
- return COORDINATOR_TOOL_RESULT_RENDERERS.agent_message(
681
+ return renderMessageResult(
754
682
  parsedResult.details,
755
683
  options,
756
684
  theme,
757
685
  parsedCall?.toolName === "agent_message" ? parsedCall.args : {},
758
686
  );
759
687
  case "subagent_wait":
760
- return COORDINATOR_TOOL_RESULT_RENDERERS.subagent_wait(
688
+ return renderWaitResult(
761
689
  parsedResult.details,
762
690
  options,
763
691
  theme,
764
692
  parsedCall?.toolName === "subagent_wait" ? parsedCall.args : {},
765
693
  );
766
694
  case "subagent_status":
767
- return COORDINATOR_TOOL_RESULT_RENDERERS.subagent_status(
768
- parsedResult.details,
769
- options,
770
- theme,
771
- );
695
+ return renderStatusResult(parsedResult.details, options, theme);
772
696
  case "subagent_cancel":
773
- return COORDINATOR_TOOL_RESULT_RENDERERS.subagent_cancel(
774
- parsedResult.details,
775
- options,
776
- theme,
777
- );
697
+ return renderCancelResult(parsedResult.details, options, theme);
778
698
  case "subagent_delete":
779
- return COORDINATOR_TOOL_RESULT_RENDERERS.subagent_delete(
780
- parsedResult.details,
781
- options,
782
- theme,
783
- );
699
+ return renderDeleteResult(parsedResult.details, options, theme);
784
700
  }
785
701
  }
786
702
 
@@ -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>;
@@ -53,7 +53,6 @@ import type {
53
53
  PersistedAgent,
54
54
  PersistedSessionIdentity,
55
55
  ProjectContextMode,
56
- RuntimeCreationRequest,
57
56
  RuntimeProfile,
58
57
  RuntimeTurnOutcome,
59
58
  } from "./minimal-subagents-types.js";
@@ -203,12 +202,14 @@ function findLatestChildSessionRecord<TRecordSchema extends TSchema>(
203
202
  customType: string,
204
203
  schema: TRecordSchema,
205
204
  ): Static<TRecordSchema> | undefined {
206
- for (let index = entries.length - 1; index >= 0; index--) {
207
- const entry = entries[index];
208
- if (!entry || entry.type !== "custom" || entry.customType !== customType) continue;
209
- if (Value.Check(schema, entry.data)) return entry.data;
210
- }
211
- return undefined;
205
+ return entries.findLast(
206
+ (
207
+ entry,
208
+ ): entry is Extract<SessionEntry, { type: "custom" }> & {
209
+ data: Static<TRecordSchema>;
210
+ } =>
211
+ entry.type === "custom" && entry.customType === customType && Value.Check(schema, entry.data),
212
+ )?.data;
212
213
  }
213
214
 
214
215
  function findLatestForkGeneration(
@@ -247,18 +248,17 @@ function findCurrentForkOwnership(
247
248
  entries: ReturnType<SessionManager["getBranch"]>,
248
249
  cloneSessionId: string,
249
250
  ): ForkOwnershipRecord | undefined {
250
- for (let index = entries.length - 1; index >= 0; index--) {
251
- const entry = entries[index];
252
- if (!entry || entry.type !== "custom" || entry.customType !== FORK_OWNERSHIP_ENTRY_TYPE)
253
- continue;
254
- if (
251
+ return entries.findLast(
252
+ (
253
+ entry,
254
+ ): entry is Extract<SessionEntry, { type: "custom" }> & {
255
+ data: ForkOwnershipRecord;
256
+ } =>
257
+ entry.type === "custom" &&
258
+ entry.customType === FORK_OWNERSHIP_ENTRY_TYPE &&
255
259
  Value.Check(ForkOwnershipRecordSchema, entry.data) &&
256
- entry.data.clone_session_id === cloneSessionId
257
- ) {
258
- return entry.data;
259
- }
260
- }
261
- return undefined;
260
+ entry.data.clone_session_id === cloneSessionId,
261
+ )?.data;
262
262
  }
263
263
 
264
264
  function verifyForkCloneProvenance(
@@ -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;
@@ -527,17 +531,6 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
527
531
  });
528
532
  }
529
533
 
530
- get sessionFile(): string {
531
- const sessionFile = this.session.sessionFile;
532
- if (!sessionFile)
533
- throw new Error("Minimal subagents child runtime lost its persistent session file");
534
- return sessionFile;
535
- }
536
-
537
- get sessionId(): string {
538
- return this.session.sessionId;
539
- }
540
-
541
534
  get sessionLeafId(): string | undefined {
542
535
  return this.session.sessionManager.getLeafId() ?? undefined;
543
536
  }
@@ -720,14 +713,6 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
720
713
  });
721
714
  }
722
715
 
723
- createRuntime(request: RuntimeCreationRequest): Promise<ChildAgentRuntime> {
724
- return this.openRuntime(request.agent);
725
- }
726
-
727
- restoreRuntime(agent: PersistedAgent): Promise<ChildAgentRuntime> {
728
- return this.openRuntime(agent);
729
- }
730
-
731
716
  resolveLaunchMissingDependencies(agent: PersistedAgent): Promise<string[]> {
732
717
  return this.findMissingDependencies(agent, false);
733
718
  }
@@ -967,7 +952,8 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
967
952
  return discovery;
968
953
  }
969
954
 
970
- private async openRuntime(agent: PersistedAgent): Promise<ChildAgentRuntime> {
955
+ /** Open one verified persisted Child Agent runtime for launch or restoration. */
956
+ async openRuntime(agent: PersistedAgent): Promise<ChildAgentRuntime> {
971
957
  if (!agent.session_file)
972
958
  throw new Error(`Minimal subagents restore: ${agent.agent_id} has no session file`);
973
959
  const model = this.modelById.get(agent.launch_contract.model);
@@ -9,7 +9,6 @@ import {
9
9
  type ToolDefinition,
10
10
  type ToolRenderResultOptions,
11
11
  } from "@earendil-works/pi-coding-agent";
12
- import type { Static } from "typebox";
13
12
  import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
14
13
  import type { MinimalSubagentsModelRole } from "./minimal-subagents-config.js";
15
14
  import {
@@ -53,27 +52,6 @@ export interface CoordinatorToolDefinitionOptions {
53
52
  onAttention?: (message: string) => void;
54
53
  }
55
54
 
56
- /** Arguments consumed by the wait tool's narrow coordinator execution seam. */
57
- export type CoordinatorWaitToolParameters = Static<
58
- ReturnType<typeof createCoordinatorToolSchemas>["subagent_wait"]
59
- >;
60
-
61
- /** Forward one typed wait-tool request without requiring an unrelated Pi execution context. */
62
- export function executeCoordinatorWaitTool(
63
- coordinator: Pick<CoordinatorToolOperations, "wait">,
64
- callerId: string,
65
- parameters: CoordinatorWaitToolParameters,
66
- signal: AbortSignal | undefined,
67
- ): Promise<WaitResult> {
68
- return coordinator.wait(
69
- callerId,
70
- parameters.agent_id,
71
- parameters.timeout_ms,
72
- signal,
73
- parameters.turn_id,
74
- );
75
- }
76
-
77
55
  function buildModelRolePromptGuidelines(
78
56
  modelRoles: readonly MinimalSubagentsModelRole[],
79
57
  ): string[] | undefined {
@@ -224,7 +202,7 @@ export function createCoordinatorToolDefinitions(
224
202
  name: "subagent_wait",
225
203
  label: "Subagent Wait",
226
204
  description:
227
- "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.",
228
206
  promptSnippet: "Wait for one direct child's exact turn",
229
207
  parameters: options.schemas.subagent_wait,
230
208
  async execute(_toolCallId, parameters, signal, onUpdate) {
@@ -243,11 +221,12 @@ export function createCoordinatorToolDefinitions(
243
221
  waitingInterval.unref?.();
244
222
  try {
245
223
  return await runCoordinatorToolActivity(options, async () => {
246
- const result = await executeCoordinatorWaitTool(
247
- options.coordinator,
224
+ const result = await options.coordinator.wait(
248
225
  options.callerId,
249
- parameters,
226
+ parameters.agent_id,
227
+ parameters.timeout_ms,
250
228
  signal,
229
+ parameters.turn_id,
251
230
  );
252
231
  return {
253
232
  ...structuredToolResult(result),
@@ -1,9 +1,6 @@
1
1
  import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import type { Usage } from "@earendil-works/pi-ai";
3
3
 
4
- /** Canonical path-like identity for one persistent subagent. */
5
- export type AgentId = string & { readonly __agentId: unique symbol };
6
-
7
4
  /** Stable identity for one prompt and its complete assistant/tool loop. */
8
5
  export type TurnId = string & { readonly __turnId: unique symbol };
9
6
 
@@ -78,8 +75,14 @@ export interface WaitMessageResult {
78
75
  message: string;
79
76
  }
80
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
+
81
84
  /** Reports one terminal child turn returned by subagent_wait. */
82
- export type WaitResult = WaitMessageResult | ({ event: "turn" } & TurnResult);
85
+ export type WaitResult = WaitMessageResult | WaitTurnResult;
83
86
 
84
87
  /** Provides bounded hierarchy, usage, and best-known Runtime Profile data for one persistent agent. */
85
88
  export interface AgentSummary extends RuntimeProfile {
@@ -190,8 +193,6 @@ export interface CoordinatorMessage {
190
193
 
191
194
  /** Process-local adapter around one SDK-created Pi child session. */
192
195
  export interface ChildAgentRuntime {
193
- readonly sessionFile: string;
194
- readonly sessionId: string;
195
196
  readonly sessionLeafId: string | undefined;
196
197
  readonly isRunning: boolean;
197
198
  runPrompt(
@@ -219,17 +220,11 @@ export interface PersistedSessionIdentity {
219
220
  sessionLeafId?: string;
220
221
  }
221
222
 
222
- /** Combines a persisted agent record with first-launch imported context. */
223
- export interface RuntimeCreationRequest {
224
- agent: PersistedAgent;
225
- importedMessages: AgentMessage[];
226
- }
227
-
228
223
  /** Pi-specific session operations injected into the pure coordinator. */
229
224
  export interface AgentSessionFactory {
230
225
  createIdentity(agent: PersistedAgent, importedMessages: AgentMessage[]): PersistedSessionIdentity;
231
- createRuntime(request: RuntimeCreationRequest): Promise<ChildAgentRuntime>;
232
- restoreRuntime(agent: PersistedAgent): Promise<ChildAgentRuntime>;
226
+ /** Open one verified persisted Child Agent runtime for launch or restoration. */
227
+ openRuntime(agent: PersistedAgent): Promise<ChildAgentRuntime>;
233
228
  resolveLaunchMissingDependencies(agent: PersistedAgent): Promise<string[]>;
234
229
  resolveRestorationMissingDependencies(agent: PersistedAgent): Promise<string[]>;
235
230
  resolveThinkingLevel(modelId: string, requested: ThinkingLevel): ThinkingLevel;
@@ -254,8 +249,6 @@ export interface RootConversationEndpoint {
254
249
  /** Queue one typed coordinator message into the root conversation. */
255
250
  queueCoordinatorMessage(message: CoordinatorMessage): Promise<void>;
256
251
  hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string, deliveryId?: string): boolean;
257
- /** Report whether automatic delivery can start a new root turn without racing an active wait. */
258
- isIdle(): boolean;
259
252
  }
260
253
 
261
254
  /** Stores root-owned agent identity, launch contract, availability, and latest activity. */