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

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
@@ -130,6 +130,11 @@ children: `subagent`, `agent_message`, `subagent_wait`, `subagent_status`,
130
130
  only the three adjacent-coordination tools: `agent_message`, `subagent_wait`,
131
131
  and `subagent_status`.
132
132
 
133
+ Targeted `subagent_status` includes `recent_activity`, a bounded tail of message
134
+ text, reasoning, tool calls, and tool results. It includes the current streaming
135
+ assistant message but omits image data. Timeout Wait Events include the same
136
+ detailed status snapshot.
137
+
133
138
  The `subagent` `tools` argument distinguishes capability presets from exact
134
139
  lists: `"read"` grants `read`, `grep`, `find`, and `ls`; `"modify"` adds
135
140
  `bash`, `edit`, and `write`; an array such as `["read"]` grants exactly the
@@ -148,6 +153,10 @@ optional `turn_id` to address an older retained turn exactly. Without it, waits
148
153
  select the oldest observable claimed or pending turn before the active/latest
149
154
  turn. A caller may have only one outstanding wait for the same source turn; a
150
155
  concurrent duplicate is rejected instead of competing for one Wait Event.
156
+ When `timeout_ms` expires, the wait returns an observational `event: "timeout"`
157
+ with the requested turn identity and the same detailed Child Agent status used
158
+ by targeted `subagent_status`. It removes only the waiter, leaving the child
159
+ running and all pending delivery unclaimed. Abort signals remain errors.
151
160
 
152
161
  The persisted Delivery Ledger records Coordination Messages, terminal results,
153
162
  globally increasing sequence, and wait ownership before delivery. Existing
@@ -155,14 +164,15 @@ items retain their sequence; gaps from skipped malformed records are valid.
155
164
  Claims can name only active, latest, or retained turns. Wait-returned messages
156
165
  retain individual delivery evidence; terminal wait ownership remains durable
157
166
  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
162
- most 20 pending wait-only terminal results per source agent; Coordination
163
- Messages are not removed by that terminal-retention limit. Delivered messages
164
- include stable delivery, source-agent, and source-turn identities in persisted
165
- details.
167
+ queue reservation while batching queued messages from one source turn into one
168
+ Pi steer. Root-bound messages remain batchable while the root turn is active; a
169
+ pending terminal result absorbs them. Child sessions drain all available steers
170
+ before the next model call. Destination-session Delivery Evidence still settles
171
+ each batched ledger item independently, preventing duplicate delivery and
172
+ unbounded checkpoint growth. The pure Delivery Ledger state machine retains at
173
+ most 20 pending wait-only terminal results per source agent; Coordination Messages
174
+ are not removed by that terminal-retention limit. Delivered messages include
175
+ stable delivery, source-agent, and source-turn identities in persisted details.
166
176
 
167
177
  Deleting a child first verifies its session header and persistent identity,
168
178
  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.2",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Persistent nested subagents with bounded delegation for Pi",
6
6
  "keywords": [
@@ -39,11 +39,10 @@ export function buildEligibleModelIds(input: {
39
39
  return [...new Set(source.map(({ provider, id }) => `${provider}/${id}`))];
40
40
  }
41
41
 
42
- /** Supplies inherited tools, the ancestor ceiling, and runtime availability for exact tool resolution. */
42
+ /** Supplies inherited tools and the ancestor ceiling for exact tool resolution. */
43
43
  export interface ToolResolutionContext {
44
44
  ordinaryTools: readonly string[];
45
45
  capabilityCeiling: readonly string[];
46
- availableTools: readonly string[];
47
46
  }
48
47
 
49
48
  /** Resolve an exact ordinary-tool contract and reject missing or over-ceiling capabilities. */
@@ -69,13 +68,9 @@ export function resolveOrdinaryToolSelection(
69
68
  `Minimal subagents ordinary tool selection: coordinator tools are injected separately and must not appear in tools: ${requestedCoordinatorTools.join(", ")}`,
70
69
  );
71
70
  }
72
- const available = new Set(context.availableTools);
71
+ // ponytail: availableTools and capabilityCeiling are identical at every production site,
72
+ // so availability and the ancestor ceiling are enforced by a single membership check.
73
73
  const ceiling = new Set(context.capabilityCeiling);
74
- const missing = uniqueRequested.filter((name) => !available.has(name));
75
- if (missing.length > 0) {
76
- throw new Error(`Minimal subagents tool resolution: unavailable tool: ${missing.join(", ")}`);
77
- }
78
-
79
74
  const exceeded = uniqueRequested.filter((name) => !ceiling.has(name));
80
75
  if (exceeded.length > 0) {
81
76
  throw new Error(`Minimal subagents capability ceiling exceeded: ${exceeded.join(", ")}`);
@@ -1,5 +1,11 @@
1
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
- import type { SessionContextMode } from "./minimal-subagents-types.js";
2
+ import { contentText, type ImageContent, type TextContent } from "@earendil-works/pi-ai";
3
+ import { truncateTail } from "@earendil-works/pi-coding-agent";
4
+ import type { RecentAgentActivity, SessionContextMode } from "./minimal-subagents-types.js";
5
+
6
+ const RECENT_AGENT_ACTIVITY_LIMIT = 12;
7
+ const RECENT_AGENT_ACTIVITY_MAX_LINES = 20;
8
+ const RECENT_AGENT_ACTIVITY_MAX_BYTES = 2 * 1024;
3
9
 
4
10
  /** Clone committed caller messages and exclude only the currently streaming assistant message. */
5
11
  export function snapshotCommittedContext(
@@ -11,6 +17,71 @@ export function snapshotCommittedContext(
11
17
  return structuredClone(committed);
12
18
  }
13
19
 
20
+ function boundedRecentActivityContent(label: string, content: string): RecentAgentActivity {
21
+ const bounded = truncateTail(content, {
22
+ maxLines: RECENT_AGENT_ACTIVITY_MAX_LINES,
23
+ maxBytes: RECENT_AGENT_ACTIVITY_MAX_BYTES,
24
+ });
25
+ return { label, content: bounded.content, truncated: bounded.truncated };
26
+ }
27
+
28
+ function visibleMessageContent(content: string | readonly (TextContent | ImageContent)[]): string {
29
+ return contentText(content, "\n\n") || "(no text content)";
30
+ }
31
+
32
+ /** Build a bounded recent activity tail from message text, reasoning, and tool work. */
33
+ export function buildRecentAgentActivity(messages: readonly AgentMessage[]): RecentAgentActivity[] {
34
+ const activity: RecentAgentActivity[] = [];
35
+ for (const message of messages) {
36
+ if (message.role === "assistant") {
37
+ for (const content of message.content) {
38
+ if (content.type === "text" && content.text) {
39
+ activity.push(boundedRecentActivityContent("assistant message", content.text));
40
+ } else if (content.type === "thinking") {
41
+ activity.push(
42
+ boundedRecentActivityContent(
43
+ "reasoning",
44
+ content.thinking ||
45
+ (content.redacted ? "[redacted reasoning]" : "(no reasoning text)"),
46
+ ),
47
+ );
48
+ } else if (content.type === "toolCall") {
49
+ activity.push(
50
+ boundedRecentActivityContent(
51
+ `tool call ${content.name}`,
52
+ JSON.stringify(content.arguments, null, 2),
53
+ ),
54
+ );
55
+ }
56
+ }
57
+ } else if (message.role === "toolResult") {
58
+ activity.push(
59
+ boundedRecentActivityContent(
60
+ `tool result ${message.toolName}${message.isError ? " (error)" : ""}`,
61
+ visibleMessageContent(message.content),
62
+ ),
63
+ );
64
+ } else if (message.role === "user" || message.role === "custom") {
65
+ activity.push(
66
+ boundedRecentActivityContent(
67
+ `${message.role} message`,
68
+ visibleMessageContent(message.content),
69
+ ),
70
+ );
71
+ } else if (message.role === "branchSummary" || message.role === "compactionSummary") {
72
+ activity.push(boundedRecentActivityContent(`${message.role} message`, message.summary));
73
+ } else if (message.role === "bashExecution") {
74
+ activity.push(
75
+ boundedRecentActivityContent(
76
+ `${message.role} message`,
77
+ `$ ${message.command}\n${message.output || "(no output)"}`,
78
+ ),
79
+ );
80
+ }
81
+ }
82
+ return activity.slice(-RECENT_AGENT_ACTIVITY_LIMIT);
83
+ }
84
+
14
85
  /** Carries the selected caller messages and whether child preparation should compact them. */
15
86
  export interface ImportedSubagentContext {
16
87
  messages: AgentMessage[];
@@ -1,5 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { assembleImportedContext, contextContainsImages } from "./minimal-subagents-context.js";
2
+ import {
3
+ assembleImportedContext,
4
+ buildRecentAgentActivity,
5
+ contextContainsImages,
6
+ } from "./minimal-subagents-context.js";
3
7
  import {
4
8
  canAgentContractSpawn,
5
9
  DEFAULT_MAX_SUBAGENT_DEPTH,
@@ -29,6 +33,7 @@ import {
29
33
  type DeliveryLedgerTransition,
30
34
  } from "./minimal-subagents-delivery-ledger.js";
31
35
  import { addCoordinatorMessageEnvelope } from "./minimal-subagents-message-envelope.js";
36
+ import { unavailableAgent } from "./minimal-subagents-sessions.js";
32
37
  import { createRegistryEvent } from "./minimal-subagents-registry.js";
33
38
  import type {
34
39
  AgentDetail,
@@ -51,7 +56,6 @@ import type {
51
56
  SpawnParameters,
52
57
  SpawnResult,
53
58
  StatusResult,
54
- TurnId,
55
59
  TurnResult,
56
60
  WaitMessageResult,
57
61
  WaitResult,
@@ -110,6 +114,24 @@ function terminalWaitResult(result: TurnResult, messages: WaitMessageResult[] =
110
114
  return messages.length === 0 ? terminal : { ...terminal, messages: structuredClone(messages) };
111
115
  }
112
116
 
117
+ function combineCoordinatorMessages(messages: readonly CoordinatorMessage[]): CoordinatorMessage {
118
+ const latest = messages.at(-1);
119
+ if (!latest) throw new Error("Minimal subagents message batch must not be empty");
120
+ if (messages.length === 1) return latest;
121
+ const references = messages.flatMap(
122
+ (message) =>
123
+ message.details.messages ??
124
+ (message.details.delivery_id
125
+ ? [{ delivery_id: message.details.delivery_id, message_id: message.details.message_id }]
126
+ : []),
127
+ );
128
+ return {
129
+ ...latest,
130
+ content: messages.map((message) => message.content).join("\n\n"),
131
+ details: references.length > 0 ? { ...latest.details, messages: references } : latest.details,
132
+ };
133
+ }
134
+
113
135
  /** One root-owned coordinator for persistent nested Pi child sessions. */
114
136
  export class MinimalSubagentsCoordinator {
115
137
  private readonly agents = new Map<string, PersistedAgent>();
@@ -185,7 +207,6 @@ export class MinimalSubagentsCoordinator {
185
207
  const ordinaryTools = resolveOrdinaryToolSelection(parameters.tools, {
186
208
  ordinaryTools: excludeCoordinatorTools(caller.ordinaryTools),
187
209
  capabilityCeiling: excludeCoordinatorTools(caller.capabilityCeiling),
188
- availableTools: excludeCoordinatorTools(caller.availableTools),
189
210
  });
190
211
  const committedMessages = structuredClone(caller.messages);
191
212
  const imported = assembleImportedContext(sessionContext, committedMessages);
@@ -278,7 +299,6 @@ export class MinimalSubagentsCoordinator {
278
299
  thinkingLevel: agent.launch_contract.thinking_level,
279
300
  ordinaryTools: [...agent.launch_contract.ordinary_tools],
280
301
  capabilityCeiling: [...agent.capability_ceiling],
281
- availableTools: [...agent.capability_ceiling],
282
302
  spawnEntryId,
283
303
  };
284
304
  }
@@ -336,7 +356,7 @@ export class MinimalSubagentsCoordinator {
336
356
  }
337
357
  }
338
358
 
339
- /** Wait for one exact turn and claim its message/result delivery from automatic fallback. */
359
+ /** Wait for one exact turn, returning detailed child status if the waiter times out. */
340
360
  wait(
341
361
  callerId: string,
342
362
  agentId: string,
@@ -387,13 +407,16 @@ export class MinimalSubagentsCoordinator {
387
407
  reject(error);
388
408
  };
389
409
  if (timeoutMs !== undefined) {
390
- waiter.timeout = setTimeout(
391
- () =>
392
- stopWaiting(
393
- new Error(`Minimal subagents wait timed out for ${agentId} after ${timeoutMs}ms`),
394
- ),
395
- timeoutMs,
396
- );
410
+ waiter.timeout = setTimeout(() => {
411
+ this.removeWaiter(key, waiter);
412
+ resolve({
413
+ event: "timeout",
414
+ agent_id: agentId,
415
+ turn_id: turnId,
416
+ timeout_ms: timeoutMs,
417
+ agent: this.buildAgentDetail(agent, false),
418
+ });
419
+ }, timeoutMs);
397
420
  }
398
421
  if (signal) {
399
422
  waiter.abortListener = () =>
@@ -474,7 +497,6 @@ export class MinimalSubagentsCoordinator {
474
497
  agent_id: agentId,
475
498
  recursive,
476
499
  deleted_agent_ids: [],
477
- tombstoned_agent_ids: [],
478
500
  trashed_session_files: [],
479
501
  failures: [],
480
502
  };
@@ -502,7 +524,6 @@ export class MinimalSubagentsCoordinator {
502
524
  this.pruneRecentMessageProjectionsForDeletedAgent(agent.agent_id);
503
525
  this.tombstones.add(agent.agent_id);
504
526
  result.deleted_agent_ids.push(agent.agent_id);
505
- result.tombstoned_agent_ids.push(agent.agent_id);
506
527
  this.dependencies.registry.append(
507
528
  createRegistryEvent(this.dependencies.registry.rootSessionId, "agent-deleted", {
508
529
  agent_ids: [agent.agent_id],
@@ -549,12 +570,8 @@ export class MinimalSubagentsCoordinator {
549
570
  this.runtimeInitializations.clear();
550
571
  this.pendingAgentIds.clear();
551
572
  this.tombstones.clear();
552
- this.deliveryLedger = createDeliveryLedger();
553
573
  this.waiters.clear();
554
- this.pendingParentMessages.clear();
555
574
  this.waitHandedDeliveryIds.clear();
556
- this.recipientQueues.clear();
557
- this.backgroundOperations.clear();
558
575
  this.automaticDeliveryKeys.clear();
559
576
  this.automaticCoordinationDeliveryIds.clear();
560
577
  this.deliveryLedger = createDeliveryLedger({
@@ -898,9 +915,8 @@ export class MinimalSubagentsCoordinator {
898
915
  return initialization;
899
916
  }
900
917
 
901
- private beginTurn(agent: PersistedAgent): TurnId {
902
- // SAFETY: The generated value embeds the canonical agent ID and a fresh turn UUID before branding.
903
- const turnId = `${agent.agent_id}:turn-${randomUUID()}` as TurnId;
918
+ private beginTurn(agent: PersistedAgent): string {
919
+ const turnId = `${agent.agent_id}:turn-${randomUUID()}`;
904
920
  const startedAt = this.now().toISOString();
905
921
  agent.active_turn_id = turnId;
906
922
  agent.active_turn_started_at = startedAt;
@@ -994,6 +1010,7 @@ export class MinimalSubagentsCoordinator {
994
1010
  if (this.automaticDeliveryKeys.has(deliveryKey)) return;
995
1011
  this.automaticDeliveryKeys.add(deliveryKey);
996
1012
  const graceMs = this.deliveryGraceMs();
1013
+ let batchedCoordinationDeliveries: PersistedCoordinationDelivery[] = [];
997
1014
  try {
998
1015
  await this.enqueueRecipientDelivery(delivery.destination_agent_id, async () => {
999
1016
  if (delivery.destination_agent_id !== "root") {
@@ -1008,11 +1025,6 @@ export class MinimalSubagentsCoordinator {
1008
1025
  this.settleDelivery(delivery);
1009
1026
  return;
1010
1027
  }
1011
- if (!this.acceptingOperations || this.shouldStopAutomaticTerminalDelivery(delivery)) return;
1012
- if (this.hasDeliveryEvidence(delivery)) {
1013
- this.settleDelivery(delivery);
1014
- return;
1015
- }
1016
1028
  const message: CoordinatorMessage = {
1017
1029
  customType: "minimal-subagents.result",
1018
1030
  content: result.output,
@@ -1026,11 +1038,28 @@ export class MinimalSubagentsCoordinator {
1026
1038
  usage: result.usage,
1027
1039
  },
1028
1040
  };
1029
- await this.deliverToRecipient(delivery.destination_agent_id, message, () =>
1030
- this.isTerminalDeliveryCurrent(delivery),
1041
+ batchedCoordinationDeliveries = this.takePendingParentDeliveryBatch(
1042
+ deliveryKey,
1043
+ delivery.destination_agent_id,
1044
+ );
1045
+ for (const batchedDelivery of batchedCoordinationDeliveries) {
1046
+ this.waitHandedDeliveryIds.add(batchedDelivery.delivery_id);
1047
+ }
1048
+ await this.deliverToRecipient(
1049
+ delivery.destination_agent_id,
1050
+ combineCoordinatorMessages([
1051
+ ...batchedCoordinationDeliveries.map((item) => item.message),
1052
+ message,
1053
+ ]),
1054
+ () =>
1055
+ this.isTerminalDeliveryCurrent(delivery) &&
1056
+ batchedCoordinationDeliveries.every((item) => this.isCoordinationDeliveryCurrent(item)),
1031
1057
  );
1032
1058
  });
1033
1059
  } catch (error) {
1060
+ for (const batchedDelivery of batchedCoordinationDeliveries) {
1061
+ this.waitHandedDeliveryIds.delete(batchedDelivery.delivery_id);
1062
+ }
1034
1063
  if (!this.isTerminalDeliveryCurrent(delivery)) return;
1035
1064
  const deliveryError = error instanceof Error ? error.message : String(error);
1036
1065
  this.deliveryLedger = setTerminalDeliveryError(
@@ -1442,11 +1471,6 @@ export class MinimalSubagentsCoordinator {
1442
1471
  elapsed_ms: elapsed ?? agent.latest_result?.elapsed_ms,
1443
1472
  latest_activity_at: agent.latest_activity_at ?? agent.created_at,
1444
1473
  task: agent.task,
1445
- latest_activity: agent.active_turn_id
1446
- ? "turn running"
1447
- : agent.latest_result
1448
- ? `turn ${agent.latest_result.status}`
1449
- : "created",
1450
1474
  child_count: directChildren.length,
1451
1475
  children,
1452
1476
  };
@@ -1454,7 +1478,8 @@ export class MinimalSubagentsCoordinator {
1454
1478
 
1455
1479
  private buildAgentDetail(agent: PersistedAgent, includeDescendants = true): AgentDetail {
1456
1480
  const summary = this.buildAgentSummary(agent, includeDescendants);
1457
- const runtimeUsage = this.runtimes.get(agent.agent_id)?.getUsage();
1481
+ const runtime = this.runtimes.get(agent.agent_id);
1482
+ const runtimeUsage = runtime?.getUsage();
1458
1483
  return {
1459
1484
  ...summary,
1460
1485
  session_file: agent.session_file,
@@ -1462,6 +1487,7 @@ export class MinimalSubagentsCoordinator {
1462
1487
  capability_ceiling: [...agent.capability_ceiling],
1463
1488
  spawn_entry_id: agent.spawn_entry_id,
1464
1489
  recent_messages: structuredClone(agent.recent_messages),
1490
+ recent_activity: buildRecentAgentActivity(runtime?.snapshotActivityMessages() ?? []),
1465
1491
  latest_result: agent.latest_result ? structuredClone(agent.latest_result) : undefined,
1466
1492
  missing_dependencies: [...agent.missing_dependencies],
1467
1493
  unavailable_reason: agent.unavailable_reason,
@@ -1585,19 +1611,7 @@ export class MinimalSubagentsCoordinator {
1585
1611
  }
1586
1612
 
1587
1613
  private createForkPlaceholder(agent: PersistedAgent, cloneError: string): PersistedAgent {
1588
- return {
1589
- ...structuredClone(agent),
1590
- session_file: undefined,
1591
- session_id: undefined,
1592
- session_leaf_id: undefined,
1593
- clone_error: cloneError,
1594
- active_turn_id: undefined,
1595
- active_turn_started_at: undefined,
1596
- latest_activity_at: this.now().toISOString(),
1597
- availability: "unavailable",
1598
- missing_dependencies: [cloneError],
1599
- unavailable_reason: cloneError,
1600
- };
1614
+ return unavailableAgent(agent, cloneError, this.now().toISOString());
1601
1615
  }
1602
1616
 
1603
1617
  private rejectAllWaiters(error: Error): void {
@@ -1765,6 +1779,7 @@ export class MinimalSubagentsCoordinator {
1765
1779
  )
1766
1780
  return;
1767
1781
 
1782
+ let automaticBatch = [delivery];
1768
1783
  const operation = this.enqueueRecipientDelivery(targetId, async () => {
1769
1784
  if (targetId !== "root") {
1770
1785
  await this.ensureRuntime(this.requireUsableAgent(targetId, "message"));
@@ -1786,25 +1801,63 @@ export class MinimalSubagentsCoordinator {
1786
1801
  pending.cancelGrace?.();
1787
1802
  pending.cancelGrace = undefined;
1788
1803
  if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
1804
+ while (
1805
+ targetId === "root" &&
1806
+ !this.dependencies.root.isIdle() &&
1807
+ !findTerminalDelivery(
1808
+ this.deliveryLedger,
1809
+ message.details.source_agent_id,
1810
+ message.details.source_turn_id,
1811
+ ) &&
1812
+ this.acceptingOperations &&
1813
+ !pending.claimed &&
1814
+ !turnClaimed()
1815
+ ) {
1816
+ await Promise.race([
1817
+ pending.claimPromise,
1818
+ new Promise((resolve) => setTimeout(resolve, 25)),
1819
+ ]);
1820
+ }
1789
1821
  if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
1790
- this.removePendingParentMessage(key, pending);
1791
- this.waitHandedDeliveryIds.add(delivery.delivery_id);
1792
- await this.deliverToRecipient(targetId, message, () =>
1793
- this.isCoordinationDeliveryCurrent(delivery),
1822
+ const terminalDelivery = findTerminalDelivery(
1823
+ this.deliveryLedger,
1824
+ message.details.source_agent_id,
1825
+ message.details.source_turn_id,
1826
+ );
1827
+ if (
1828
+ terminalDelivery?.destination_agent_id === targetId &&
1829
+ terminalDelivery.path === "message"
1830
+ ) {
1831
+ for (const queued of this.pendingParentMessages.get(key) ?? []) {
1832
+ queued.cancelGrace?.();
1833
+ queued.releaseClaim();
1834
+ }
1835
+ return;
1836
+ }
1837
+ automaticBatch = this.takePendingParentDeliveryBatch(key, targetId);
1838
+ for (const batchedDelivery of automaticBatch) {
1839
+ this.waitHandedDeliveryIds.add(batchedDelivery.delivery_id);
1840
+ }
1841
+ if (automaticBatch.length === 0) return;
1842
+ await this.deliverToRecipient(
1843
+ targetId,
1844
+ combineCoordinatorMessages(automaticBatch.map((item) => item.message)),
1845
+ () => automaticBatch.every((item) => this.isCoordinationDeliveryCurrent(item)),
1794
1846
  );
1795
1847
  });
1796
1848
  void operation.catch((cause) => {
1797
- this.removePendingParentMessage(key, pending);
1798
- this.waitHandedDeliveryIds.delete(delivery.delivery_id);
1799
- if (!this.isCoordinationDeliveryCurrent(delivery)) return;
1800
1849
  const deliveryError = cause instanceof Error ? cause.message : String(cause);
1801
- this.deliveryLedger = setCoordinationDeliveryError(
1802
- this.deliveryLedger,
1803
- delivery.delivery_id,
1804
- deliveryError,
1805
- );
1806
- const current = findCoordinationDelivery(this.deliveryLedger, delivery.delivery_id);
1807
- if (current) this.persistCoordinationDeliveryState(current);
1850
+ for (const batchedDelivery of automaticBatch) {
1851
+ this.waitHandedDeliveryIds.delete(batchedDelivery.delivery_id);
1852
+ if (!this.isCoordinationDeliveryCurrent(batchedDelivery)) continue;
1853
+ this.deliveryLedger = setCoordinationDeliveryError(
1854
+ this.deliveryLedger,
1855
+ batchedDelivery.delivery_id,
1856
+ deliveryError,
1857
+ );
1858
+ const current = findCoordinationDelivery(this.deliveryLedger, batchedDelivery.delivery_id);
1859
+ if (current) this.persistCoordinationDeliveryState(current);
1860
+ }
1808
1861
  this.dependencies.notify?.({
1809
1862
  type: "failure",
1810
1863
  agentId: message.details.source_agent_id,
@@ -1815,6 +1868,25 @@ export class MinimalSubagentsCoordinator {
1815
1868
  });
1816
1869
  }
1817
1870
 
1871
+ private takePendingParentDeliveryBatch(
1872
+ key: string,
1873
+ targetId: string,
1874
+ ): PersistedCoordinationDelivery[] {
1875
+ const batch = (this.pendingParentMessages.get(key) ?? []).filter(
1876
+ (pending) => pending.destinationAgentId === targetId && !pending.claimed,
1877
+ );
1878
+ for (const pending of batch) {
1879
+ pending.claimed = true;
1880
+ pending.cancelGrace?.();
1881
+ pending.releaseClaim();
1882
+ this.removePendingParentMessage(key, pending);
1883
+ }
1884
+ return batch.flatMap((pending) => {
1885
+ const delivery = findCoordinationDelivery(this.deliveryLedger, pending.deliveryId);
1886
+ return delivery ? [delivery] : [];
1887
+ });
1888
+ }
1889
+
1818
1890
  private removePendingParentMessage(key: string, pending: PendingParentMessage): void {
1819
1891
  const pendingMessages = this.pendingParentMessages.get(key);
1820
1892
  if (!pendingMessages) return;