@ian-pascoe/pi-minimal-subagents 0.1.0 → 0.2.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.
@@ -8,9 +8,32 @@ import {
8
8
  getSubagentDepth,
9
9
  resolveOrdinaryToolSelection,
10
10
  } from "./minimal-subagents-capabilities.js";
11
+ import {
12
+ addCoordinationDelivery,
13
+ addTerminalDelivery,
14
+ claimDeliveryLedgerTurn,
15
+ createDeliveryLedger,
16
+ deliveryLedgerSnapshot,
17
+ findCoordinationDelivery,
18
+ findTerminalDelivery,
19
+ isDeliveryLedgerTurnClaimed,
20
+ pruneDeliveryLedgerAgents,
21
+ releaseEmptyDeliveryLedgerTurn,
22
+ selectObservableDeliveryTurn,
23
+ setCoordinationDeliveryError,
24
+ setCoordinationDeliveryPath,
25
+ setTerminalDeliveryError,
26
+ setTerminalDeliveryPath,
27
+ settleCoordinationDelivery,
28
+ settleTerminalDelivery,
29
+ type DeliveryLedger,
30
+ type DeliveryLedgerTransition,
31
+ } from "./minimal-subagents-delivery-ledger.js";
32
+ import { addCoordinatorMessageEnvelope } from "./minimal-subagents-message-envelope.js";
11
33
  import { createRegistryEvent } from "./minimal-subagents-registry.js";
12
34
  import type {
13
35
  AgentDetail,
36
+ AgentMessageDisposition,
14
37
  AgentMessageResult,
15
38
  AgentSessionFactory,
16
39
  AgentSummary,
@@ -23,6 +46,7 @@ import type {
23
46
  ForkSnapshot,
24
47
  HierarchyStatusResult,
25
48
  PersistedAgent,
49
+ PersistedCoordinationDelivery,
26
50
  PersistedDelivery,
27
51
  RegistrySnapshot,
28
52
  SpawnParameters,
@@ -30,6 +54,7 @@ import type {
30
54
  StatusResult,
31
55
  TurnId,
32
56
  TurnResult,
57
+ WaitResult,
33
58
  } from "./minimal-subagents-types.js";
34
59
 
35
60
  const FRIENDLY_AGENT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
@@ -44,13 +69,28 @@ interface MessageParameters {
44
69
 
45
70
  interface TurnWaiter {
46
71
  callerId: string;
47
- resolve: (result: TurnResult) => void;
72
+ resolve: (result: WaitResult) => void;
48
73
  reject: (error: Error) => void;
49
74
  timeout?: ReturnType<typeof setTimeout>;
50
75
  abortSignal?: AbortSignal;
51
76
  abortListener?: () => void;
52
77
  }
53
78
 
79
+ interface PendingParentMessage {
80
+ deliveryId: string;
81
+ message: CoordinatorMessage;
82
+ destinationAgentId: string;
83
+ claimed: boolean;
84
+ claimPromise: Promise<void>;
85
+ releaseClaim: () => void;
86
+ cancelGrace?: () => void;
87
+ }
88
+
89
+ interface CancelableWait {
90
+ promise: Promise<void>;
91
+ cancel: () => void;
92
+ }
93
+
54
94
  function agentDeliveryKey(agentId: string, turnId: string): string {
55
95
  return `${agentId}\u0000${turnId}`;
56
96
  }
@@ -70,6 +110,10 @@ function terminalTurnResult(
70
110
  };
71
111
  }
72
112
 
113
+ function terminalWaitResult(result: TurnResult): WaitResult {
114
+ return { event: "turn", ...structuredClone(result) };
115
+ }
116
+
73
117
  /** One root-owned coordinator for persistent nested Pi child sessions. */
74
118
  export class MinimalSubagentsCoordinator {
75
119
  private readonly agents = new Map<string, PersistedAgent>();
@@ -78,11 +122,18 @@ export class MinimalSubagentsCoordinator {
78
122
  private readonly importedMessages = new Map<string, AgentMessage[]>();
79
123
  private readonly tombstones = new Set<string>();
80
124
  private readonly pendingAgentIds = new Set<string>();
81
- private readonly deliveries = new Map<string, PersistedDelivery>();
125
+ private deliveryLedger: DeliveryLedger = createDeliveryLedger();
82
126
  private readonly waiters = new Map<string, Set<TurnWaiter>>();
83
- private readonly recipientQueues = new Map<string, Promise<void>>();
127
+ private readonly pendingParentMessages = new Map<string, PendingParentMessage[]>();
128
+ private readonly recipientQueues = new Map<string, Promise<unknown>>();
129
+ private readonly recipientIdleWaiters = new Map<string, Set<() => void>>();
130
+ private readonly automaticDeliveryKeys = new Set<string>();
131
+ private readonly automaticCoordinationDeliveryIds = new Set<string>();
132
+ private readonly waitHandedDeliveryIds = new Set<string>();
133
+ private readonly automaticDeliveryClaimWaiters = new Map<string, Set<() => void>>();
84
134
  private readonly backgroundOperations = new Set<Promise<void>>();
85
135
  private acceptingOperations = true;
136
+ private lifecycleEpoch = 0;
86
137
  private shutdownPromise?: Promise<void>;
87
138
 
88
139
  constructor(private readonly dependencies: CoordinatorDependencies) {}
@@ -93,10 +144,11 @@ export class MinimalSubagentsCoordinator {
93
144
 
94
145
  /** Return a serializable complete hierarchy checkpoint without process-local runtimes. */
95
146
  snapshot(): RegistrySnapshot {
147
+ const ledger = deliveryLedgerSnapshot(this.deliveryLedger);
96
148
  return {
97
149
  agents: [...this.agents.values()].map((agent) => structuredClone(agent)),
98
150
  tombstones: [...this.tombstones],
99
- deliveries: [...this.deliveries.values()].map((delivery) => structuredClone(delivery)),
151
+ ...ledger,
100
152
  };
101
153
  }
102
154
 
@@ -191,8 +243,14 @@ export class MinimalSubagentsCoordinator {
191
243
  } finally {
192
244
  this.pendingAgentIds.delete(agentId);
193
245
  }
246
+ if (!identity.sessionLeafId) {
247
+ throw new Error(
248
+ `Minimal subagents persistent identity: no selected session leaf for ${agent.agent_id}`,
249
+ );
250
+ }
194
251
  agent.session_file = identity.sessionFile;
195
252
  agent.session_id = identity.sessionId;
253
+ agent.session_leaf_id = identity.sessionLeafId;
196
254
  this.agents.set(agentId, agent);
197
255
  this.importedMessages.set(agentId, imported.messages);
198
256
  this.dependencies.registry.append(
@@ -233,7 +291,7 @@ export class MinimalSubagentsCoordinator {
233
291
  };
234
292
  }
235
293
 
236
- /** Send one steer-only coordination message to an authorized adjacent agent. */
294
+ /** Send one coordination message to an authorized adjacent agent. */
237
295
  async sendAgentMessage(
238
296
  callerId: string,
239
297
  parameters: MessageParameters,
@@ -242,39 +300,88 @@ export class MinimalSubagentsCoordinator {
242
300
  this.assertAccepting();
243
301
  this.assertCallerExists(callerId);
244
302
  const targetId = this.resolveMessageTarget(callerId, parameters.agent_id);
303
+ const messageId = randomUUID();
304
+ const message = this.createExplicitMessage(
305
+ callerId,
306
+ targetId,
307
+ sourceTurnId,
308
+ messageId,
309
+ parameters.message,
310
+ );
311
+ const sourceAgent = callerId === "root" ? undefined : this.requireAgent(callerId);
312
+ const sentToDirectParent = sourceAgent?.parent_id === targetId;
313
+ const delivery = this.persistCoordinationDelivery(message, targetId);
245
314
  try {
246
- await this.enqueueRecipientDelivery(targetId, async () => {
247
- await this.deliverExplicitMessage(callerId, targetId, sourceTurnId, parameters.message);
248
- });
249
- return { agent_id: targetId, delivered: true };
315
+ this.recordRecentMessage(targetId, message);
316
+ if (sentToDirectParent) {
317
+ if (this.resolveWaiterWithMessage(message, delivery)) {
318
+ return { agent_id: targetId, message_id: messageId, disposition: "delivered-via-wait" };
319
+ }
320
+ this.queuePendingParentMessage(targetId, message, delivery);
321
+ return { agent_id: targetId, message_id: messageId, disposition: "queued" };
322
+ }
323
+ const disposition = await this.enqueueRecipientDelivery(targetId, async () =>
324
+ this.deliverExplicitMessage(message, targetId, delivery),
325
+ );
326
+ return { agent_id: targetId, message_id: messageId, disposition };
250
327
  } catch (error) {
328
+ const deliveryError = error instanceof Error ? error.message : String(error);
329
+ if (this.isCoordinationDeliveryCurrent(delivery)) {
330
+ this.deliveryLedger = setCoordinationDeliveryError(
331
+ this.deliveryLedger,
332
+ delivery.delivery_id,
333
+ deliveryError,
334
+ );
335
+ const current = findCoordinationDelivery(this.deliveryLedger, delivery.delivery_id);
336
+ if (current) this.persistCoordinationDeliveryState(current);
337
+ }
251
338
  return {
252
339
  agent_id: targetId,
253
- delivered: false,
254
- error: error instanceof Error ? error.message : String(error),
340
+ message_id: messageId,
341
+ disposition: "failed",
342
+ error: deliveryError,
255
343
  };
256
344
  }
257
345
  }
258
346
 
259
- /** Wait for the exact active turn captured at invocation, or return the latest idle result. */
347
+ /** Wait for one exact turn and claim its message/result delivery from automatic fallback. */
260
348
  wait(
261
349
  callerId: string,
262
350
  agentId: string,
263
351
  timeoutMs?: number,
264
352
  signal?: AbortSignal,
265
- ): Promise<TurnResult> {
353
+ requestedTurnId?: string,
354
+ ): Promise<WaitResult> {
266
355
  this.assertAccepting();
267
356
  this.assertCallerExists(callerId);
268
357
  this.assertCallerTargetsDirectChild(callerId, agentId, "wait");
269
358
  const agent = this.requireUsableAgent(agentId, "wait");
270
- if (!agent.active_turn_id) {
271
- if (agent.latest_result) return Promise.resolve(structuredClone(agent.latest_result));
359
+ const turnId = requestedTurnId ?? this.selectObservableTurnId(agentId, callerId);
360
+ if (!turnId) {
272
361
  return Promise.reject(new Error(`Minimal subagents wait: ${agentId} has no turn to observe`));
273
362
  }
274
- const turnId = agent.active_turn_id;
275
363
  const key = agentDeliveryKey(agentId, turnId);
364
+ if ([...(this.waiters.get(key) ?? [])].some((waiter) => waiter.callerId === callerId)) {
365
+ return Promise.reject(
366
+ new Error(`Minimal subagents duplicate wait: ${callerId} is already waiting for ${turnId}`),
367
+ );
368
+ }
369
+ const pendingMessage = this.claimPendingParentMessage(callerId, agentId, turnId);
370
+ if (pendingMessage) return Promise.resolve(pendingMessage);
371
+ const retainedResult =
372
+ findTerminalDelivery(this.deliveryLedger, agentId, turnId)?.result ??
373
+ (agent.latest_result?.turn_id === turnId ? agent.latest_result : undefined);
374
+ if (retainedResult) {
375
+ this.claimTerminalDelivery(callerId, retainedResult);
376
+ return Promise.resolve(terminalWaitResult(retainedResult));
377
+ }
378
+ if (agent.active_turn_id !== turnId) {
379
+ return Promise.reject(
380
+ new Error(`Minimal subagents wait: turn ${turnId} is no longer retained for ${agentId}`),
381
+ );
382
+ }
276
383
 
277
- return new Promise<TurnResult>((resolve, reject) => {
384
+ return new Promise<WaitResult>((resolve, reject) => {
278
385
  const waiter: TurnWaiter = { callerId, resolve, reject, abortSignal: signal };
279
386
  let turnWaiters = this.waiters.get(key);
280
387
  if (!turnWaiters) {
@@ -394,11 +501,13 @@ export class MinimalSubagentsCoordinator {
394
501
  runtime?.dispose();
395
502
  this.runtimes.delete(agent.agent_id);
396
503
  if (agent.session_file) {
397
- await this.dependencies.sessions.trashSessionFile(agent.session_file);
504
+ await this.dependencies.sessions.trashSession(agent);
398
505
  result.trashed_session_files.push(agent.session_file);
399
506
  }
400
507
  this.agents.delete(agent.agent_id);
401
508
  this.importedMessages.delete(agent.agent_id);
509
+ this.pruneDeliveryStateForDeletedAgent(agent.agent_id);
510
+ this.pruneRecentMessageProjectionsForDeletedAgent(agent.agent_id);
402
511
  this.tombstones.add(agent.agent_id);
403
512
  result.deleted_agent_ids.push(agent.agent_id);
404
513
  result.tombstoned_agent_ids.push(agent.agent_id);
@@ -433,26 +542,47 @@ export class MinimalSubagentsCoordinator {
433
542
 
434
543
  /** Restore non-deleted descendants, interrupt unfinished work, and reconcile pending successful output. */
435
544
  async restore(snapshot: RegistrySnapshot): Promise<void> {
436
- for (const runtime of this.runtimes.values()) runtime.dispose();
545
+ const restoreEpoch = ++this.lifecycleEpoch;
546
+ this.rejectAllWaiters(
547
+ new Error("Minimal subagents wait cancelled because the session branch changed"),
548
+ );
549
+ const abandonedRuntimes = [...this.runtimes.values()];
437
550
  this.agents.clear();
551
+ this.deliveryLedger = createDeliveryLedger();
552
+ this.pendingParentMessages.clear();
553
+ this.releaseAllRecipientIdleWaiters();
554
+ this.recipientQueues.clear();
555
+ this.backgroundOperations.clear();
556
+ await Promise.allSettled(
557
+ abandonedRuntimes.map((runtime) => (runtime.isRunning ? runtime.abort() : Promise.resolve())),
558
+ );
559
+ for (const runtime of abandonedRuntimes) runtime.dispose();
438
560
  this.runtimes.clear();
439
561
  this.runtimeInitializations.clear();
440
562
  this.importedMessages.clear();
441
563
  this.pendingAgentIds.clear();
442
564
  this.tombstones.clear();
443
- this.deliveries.clear();
565
+ this.deliveryLedger = createDeliveryLedger();
444
566
  this.waiters.clear();
567
+ this.pendingParentMessages.clear();
568
+ this.waitHandedDeliveryIds.clear();
569
+ this.releaseAllRecipientIdleWaiters();
570
+ this.recipientQueues.clear();
571
+ this.backgroundOperations.clear();
572
+ this.automaticDeliveryKeys.clear();
573
+ this.automaticCoordinationDeliveryIds.clear();
574
+ this.automaticDeliveryClaimWaiters.clear();
575
+ this.deliveryLedger = createDeliveryLedger({
576
+ deliveries: snapshot.deliveries,
577
+ coordination_deliveries: snapshot.coordination_deliveries,
578
+ wait_claimed_turns: snapshot.wait_claimed_turns,
579
+ next_delivery_sequence: snapshot.next_delivery_sequence,
580
+ });
445
581
  this.acceptingOperations = true;
446
582
  this.shutdownPromise = undefined;
447
583
 
448
584
  for (const agent of snapshot.agents) this.agents.set(agent.agent_id, structuredClone(agent));
449
585
  for (const tombstone of snapshot.tombstones) this.tombstones.add(tombstone);
450
- for (const delivery of snapshot.deliveries) {
451
- this.deliveries.set(
452
- agentDeliveryKey(delivery.source_agent_id, delivery.source_turn_id),
453
- structuredClone(delivery),
454
- );
455
- }
456
586
 
457
587
  for (const agent of this.agents.values()) {
458
588
  if (agent.active_turn_id) {
@@ -471,28 +601,41 @@ export class MinimalSubagentsCoordinator {
471
601
  });
472
602
  }
473
603
  const previousAvailability = agent.availability;
474
- const missing = agent.clone_error
475
- ? [agent.clone_error]
476
- : await this.dependencies.sessions.resolveRestorationMissingDependencies(agent);
477
- if (missing.length > 0 || !agent.session_file) {
478
- agent.availability = "unavailable";
479
- if (previousAvailability !== "unavailable")
480
- agent.latest_activity_at = this.now().toISOString();
481
- agent.missing_dependencies = missing.length > 0 ? missing : agent.missing_dependencies;
482
- agent.unavailable_reason =
483
- agent.clone_error ??
484
- (missing.length > 0
485
- ? `Missing dependencies: ${missing.join(", ")}`
486
- : (agent.unavailable_reason ?? `No persistent session exists for ${agent.agent_id}`));
487
- this.dependencies.notify?.({
488
- type: "unavailable",
489
- agentId: agent.agent_id,
490
- message: `${agent.agent_id} unavailable: ${agent.unavailable_reason}`,
491
- });
492
- continue;
493
- }
494
604
  try {
495
- this.runtimes.set(agent.agent_id, await this.dependencies.sessions.restoreRuntime(agent));
605
+ const missing = agent.clone_error
606
+ ? [agent.clone_error]
607
+ : await this.dependencies.sessions.resolveRestorationMissingDependencies(agent);
608
+ if (restoreEpoch !== this.lifecycleEpoch) return;
609
+ if (missing.length > 0 || !agent.session_file) {
610
+ agent.availability = "unavailable";
611
+ if (previousAvailability !== "unavailable")
612
+ agent.latest_activity_at = this.now().toISOString();
613
+ agent.missing_dependencies = missing.length > 0 ? missing : agent.missing_dependencies;
614
+ agent.unavailable_reason =
615
+ agent.clone_error ??
616
+ (missing.length > 0
617
+ ? `Missing dependencies: ${missing.join(", ")}`
618
+ : (agent.unavailable_reason ?? `No persistent session exists for ${agent.agent_id}`));
619
+ this.dependencies.notify?.({
620
+ type: "unavailable",
621
+ agentId: agent.agent_id,
622
+ message: `${agent.agent_id} unavailable: ${agent.unavailable_reason}`,
623
+ });
624
+ continue;
625
+ }
626
+ const runtime = await this.dependencies.sessions.restoreRuntime(agent);
627
+ if (restoreEpoch !== this.lifecycleEpoch) {
628
+ runtime.dispose();
629
+ return;
630
+ }
631
+ if (!runtime.sessionLeafId) {
632
+ runtime.dispose();
633
+ throw new Error(
634
+ `Minimal subagents session restoration: no selected session leaf for ${agent.agent_id}`,
635
+ );
636
+ }
637
+ this.runtimes.set(agent.agent_id, runtime);
638
+ agent.session_leaf_id = runtime.sessionLeafId;
496
639
  agent.availability = "available";
497
640
  if (previousAvailability !== "available")
498
641
  agent.latest_activity_at = this.now().toISOString();
@@ -504,6 +647,7 @@ export class MinimalSubagentsCoordinator {
504
647
  message: `Restored ${agent.agent_id}`,
505
648
  });
506
649
  } catch (error) {
650
+ if (restoreEpoch !== this.lifecycleEpoch) return;
507
651
  agent.availability = "unavailable";
508
652
  if (previousAvailability !== "unavailable")
509
653
  agent.latest_activity_at = this.now().toISOString();
@@ -515,7 +659,7 @@ export class MinimalSubagentsCoordinator {
515
659
  });
516
660
  }
517
661
  }
518
- await this.reconcileDeliveries(true);
662
+ if (restoreEpoch === this.lifecycleEpoch) await this.reconcileDeliveries(true);
519
663
  }
520
664
 
521
665
  /** Schedule delivery reconciliation as coordinator-owned work drained during shutdown. */
@@ -525,8 +669,51 @@ export class MinimalSubagentsCoordinator {
525
669
 
526
670
  /** Reconcile durable destination evidence and replay only successful undelivered output. */
527
671
  async reconcileDeliveries(replayMissing = false): Promise<void> {
528
- for (const delivery of this.deliveries.values()) {
529
- if (delivery.settled) continue;
672
+ const pendingItems = [
673
+ ...this.deliveryLedger.coordinationDeliveries.map((delivery) => ({
674
+ kind: "coordination" as const,
675
+ sequence: delivery.sequence,
676
+ delivery,
677
+ })),
678
+ ...this.deliveryLedger.terminalDeliveries.map((delivery) => ({
679
+ kind: "terminal" as const,
680
+ sequence: delivery.sequence ?? Number.MAX_SAFE_INTEGER,
681
+ delivery,
682
+ })),
683
+ ].sort((left, right) => left.sequence - right.sequence);
684
+ const scheduled: Promise<void>[] = [];
685
+
686
+ for (const item of pendingItems) {
687
+ if (item.kind === "coordination") {
688
+ const delivery = item.delivery;
689
+ if (this.hasCoordinationDeliveryEvidence(delivery)) {
690
+ this.settleCoordinationDelivery(delivery);
691
+ continue;
692
+ }
693
+ if (!replayMissing || delivery.path === "wait") continue;
694
+ const source = this.agents.get(delivery.message.details.source_agent_id);
695
+ if (source?.parent_id === delivery.destination_agent_id) {
696
+ const key = agentDeliveryKey(
697
+ delivery.message.details.source_agent_id,
698
+ delivery.message.details.source_turn_id,
699
+ );
700
+ const alreadyQueued = this.pendingParentMessages
701
+ .get(key)
702
+ ?.some((pending) => pending.deliveryId === delivery.delivery_id);
703
+ if (!alreadyQueued && !this.waitHandedDeliveryIds.has(delivery.delivery_id)) {
704
+ this.queuePendingParentMessage(
705
+ delivery.destination_agent_id,
706
+ delivery.message,
707
+ delivery,
708
+ );
709
+ }
710
+ } else {
711
+ scheduled.push(this.replayCoordinationDelivery(delivery));
712
+ }
713
+ continue;
714
+ }
715
+
716
+ const delivery = item.delivery;
530
717
  const agent = this.agents.get(delivery.source_agent_id);
531
718
  const result = delivery.result ?? agent?.latest_result;
532
719
  if (!result || result.status !== "completed" || result.turn_id !== delivery.source_turn_id)
@@ -535,25 +722,24 @@ export class MinimalSubagentsCoordinator {
535
722
  this.settleDelivery(delivery);
536
723
  continue;
537
724
  }
538
- if (!replayMissing) continue;
539
- delivery.path = "message";
540
- this.dependencies.registry.append(
541
- createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pending", {
542
- delivery,
543
- }),
544
- );
545
- await this.deliverAutomaticResult(result, delivery);
725
+ if (!replayMissing || delivery.path === "wait") continue;
726
+ scheduled.push(this.deliverAutomaticResult(result, delivery));
546
727
  }
728
+ await Promise.allSettled(scheduled);
729
+ }
730
+
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();
547
736
  }
548
737
 
549
738
  /** Clone complete child leaves for root fork ownership without ever sharing source session paths. */
550
739
  async prepareFork(sourceRootSessionFile: string): Promise<ForkSnapshot> {
551
- this.acceptingOperations = false;
552
740
  const activeRootChildren = this.childrenOf("root");
553
741
  for (const child of activeRootChildren) await this.cancelDuringShutdown(child.agent_id);
554
- await Promise.allSettled(this.runtimeInitializations.values());
555
- await Promise.allSettled(this.backgroundOperations);
556
- await Promise.allSettled(this.recipientQueues.values());
742
+ await this.waitForSettledOperations();
557
743
  const forkAgents: PersistedAgent[] = [];
558
744
  const failedSubtrees = new Set<string>();
559
745
 
@@ -569,10 +755,16 @@ export class MinimalSubagentsCoordinator {
569
755
  }
570
756
  try {
571
757
  const clone = await this.dependencies.sessions.cloneSession(agent);
758
+ if (!clone.sessionLeafId) {
759
+ throw new Error(
760
+ `Minimal subagents fork clone: no selected session leaf for ${agent.agent_id}`,
761
+ );
762
+ }
572
763
  forkAgents.push({
573
764
  ...structuredClone(agent),
574
765
  session_file: clone.sessionFile,
575
766
  session_id: clone.sessionId,
767
+ session_leaf_id: clone.sessionLeafId,
576
768
  active_turn_id: undefined,
577
769
  active_turn_started_at: undefined,
578
770
  });
@@ -588,11 +780,12 @@ export class MinimalSubagentsCoordinator {
588
780
  }
589
781
  }
590
782
 
783
+ const snapshot = this.snapshot();
591
784
  return {
785
+ ...snapshot,
592
786
  source_root_session_file: sourceRootSessionFile,
787
+ source_root_session_id: this.dependencies.registry.rootSessionId,
593
788
  agents: forkAgents,
594
- tombstones: [...this.tombstones],
595
- deliveries: [...this.deliveries.values()].map((delivery) => structuredClone(delivery)),
596
789
  };
597
790
  }
598
791
 
@@ -600,6 +793,7 @@ export class MinimalSubagentsCoordinator {
600
793
  shutdown(): Promise<void> {
601
794
  if (this.shutdownPromise) return this.shutdownPromise;
602
795
  this.acceptingOperations = false;
796
+ this.releaseAllRecipientIdleWaiters();
603
797
  this.shutdownPromise = this.finishShutdown();
604
798
  return this.shutdownPromise;
605
799
  }
@@ -684,7 +878,7 @@ export class MinimalSubagentsCoordinator {
684
878
  return;
685
879
  }
686
880
  const outcome = await runtime.runPrompt(task, compact, callerModel, callerThinkingLevel);
687
- if (agent.active_turn_id !== turnId) return;
881
+ if (this.agents.get(agentId) !== agent || agent.active_turn_id !== turnId) return;
688
882
  this.settleTurn(agent, turnId, terminalTurnResult(agentId, turnId, outcome));
689
883
  } catch (error) {
690
884
  if (this.agents.get(agentId) !== agent || agent.active_turn_id !== turnId) return;
@@ -733,6 +927,7 @@ export class MinimalSubagentsCoordinator {
733
927
  }
734
928
 
735
929
  private beginTurn(agent: PersistedAgent): TurnId {
930
+ // SAFETY: The generated value embeds the canonical agent ID and a fresh turn UUID before branding.
736
931
  const turnId = `${agent.agent_id}:turn-${randomUUID()}` as TurnId;
737
932
  const startedAt = this.now().toISOString();
738
933
  agent.active_turn_id = turnId;
@@ -764,34 +959,44 @@ export class MinimalSubagentsCoordinator {
764
959
  agent.active_turn_started_at = undefined;
765
960
  agent.latest_activity_at = settledAt.toISOString();
766
961
  agent.latest_result = structuredClone(result);
962
+ const runtimeLeafId = this.runtimes.get(agent.agent_id)?.sessionLeafId;
963
+ if (runtimeLeafId) agent.session_leaf_id = runtimeLeafId;
767
964
  this.dependencies.registry.append(
768
- createRegistryEvent(this.dependencies.registry.rootSessionId, "turn-settled", { result }),
965
+ createRegistryEvent(this.dependencies.registry.rootSessionId, "turn-settled", {
966
+ result,
967
+ settled_at: settledAt.toISOString(),
968
+ session_leaf_id: runtimeLeafId,
969
+ }),
769
970
  );
770
971
  const waiterKey = agentDeliveryKey(agent.agent_id, turnId);
771
972
  const turnWaiters = this.waiters.get(waiterKey);
772
973
  const directParentWaited = [...(turnWaiters ?? [])].some(
773
974
  (waiter) => waiter.callerId === agent.parent_id,
774
975
  );
976
+ if (directParentWaited && result.status === "completed") {
977
+ this.claimDeliveryTurn(agent.agent_id, turnId);
978
+ }
979
+ const parentClaimedTurn =
980
+ directParentWaited ||
981
+ isDeliveryLedgerTurnClaimed(this.deliveryLedger, agent.agent_id, turnId);
775
982
  for (const waiter of turnWaiters ?? []) {
776
983
  this.removeWaiter(waiterKey, waiter);
777
- waiter.resolve(structuredClone(result));
984
+ waiter.resolve(terminalWaitResult(result));
778
985
  }
779
986
  if (result.status === "completed") {
780
- const delivery: PersistedDelivery = {
781
- source_agent_id: agent.agent_id,
782
- source_turn_id: turnId,
783
- destination_agent_id: agent.parent_id,
784
- path: directParentWaited ? "wait" : "message",
785
- settled: false,
786
- result: structuredClone(result),
787
- };
788
- this.deliveries.set(waiterKey, delivery);
987
+ const added = addTerminalDelivery(this.deliveryLedger, {
988
+ destinationAgentId: agent.parent_id,
989
+ path: parentClaimedTurn ? "wait" : "message",
990
+ result,
991
+ });
992
+ this.applyDeliveryLedgerTransition(added);
993
+ const delivery = added.delivery;
789
994
  this.dependencies.registry.append(
790
995
  createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pending", {
791
996
  delivery,
792
997
  }),
793
998
  );
794
- if (!directParentWaited) {
999
+ if (!parentClaimedTurn) {
795
1000
  this.trackBackgroundOperation(this.deliverAutomaticResult(result, delivery));
796
1001
  }
797
1002
  this.dependencies.notify?.({
@@ -806,22 +1011,46 @@ export class MinimalSubagentsCoordinator {
806
1011
  message: `${agent.agent_id} failed: ${result.error ?? "unknown error"}`,
807
1012
  });
808
1013
  }
1014
+ if (result.status !== "completed") this.removeSettledEmptyTurnClaim(agent.agent_id, turnId);
1015
+ this.markRecipientIdle(agent.agent_id);
809
1016
  }
810
1017
 
811
1018
  private async deliverAutomaticResult(
812
1019
  result: TurnResult,
813
1020
  delivery: PersistedDelivery,
814
1021
  ): Promise<void> {
815
- const graceMs =
816
- this.dependencies.automaticDeliveryGraceMs ?? DEFAULT_AUTOMATIC_DELIVERY_GRACE_MS;
817
- if (graceMs > 0) await new Promise((resolve) => setTimeout(resolve, graceMs));
818
- if (delivery.settled) return;
819
- if (this.hasDeliveryEvidence(delivery)) {
820
- this.settleDelivery(delivery);
821
- return;
822
- }
1022
+ const deliveryKey = agentDeliveryKey(delivery.source_agent_id, delivery.source_turn_id);
1023
+ if (this.automaticDeliveryKeys.has(deliveryKey)) return;
1024
+ this.automaticDeliveryKeys.add(deliveryKey);
1025
+ const graceMs = this.deliveryGraceMs();
823
1026
  try {
824
1027
  await this.enqueueRecipientDelivery(delivery.destination_agent_id, async () => {
1028
+ if (delivery.destination_agent_id !== "root") {
1029
+ await this.ensureRuntime(
1030
+ this.requireUsableAgent(delivery.destination_agent_id, "message"),
1031
+ );
1032
+ if (!this.isTerminalDeliveryCurrent(delivery)) return;
1033
+ }
1034
+ if (graceMs > 0) await new Promise((resolve) => setTimeout(resolve, graceMs));
1035
+ if (!this.acceptingOperations || this.shouldStopAutomaticTerminalDelivery(delivery)) return;
1036
+ if (this.hasDeliveryEvidence(delivery)) {
1037
+ this.settleDelivery(delivery);
1038
+ return;
1039
+ }
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
+ if (!this.acceptingOperations || this.shouldStopAutomaticTerminalDelivery(delivery)) return;
1050
+ if (this.hasDeliveryEvidence(delivery)) {
1051
+ this.settleDelivery(delivery);
1052
+ return;
1053
+ }
825
1054
  const message: CoordinatorMessage = {
826
1055
  customType: "minimal-subagents.result",
827
1056
  content: result.output,
@@ -829,82 +1058,265 @@ export class MinimalSubagentsCoordinator {
829
1058
  source_agent_id: delivery.source_agent_id,
830
1059
  destination_agent_id: delivery.destination_agent_id,
831
1060
  source_turn_id: result.turn_id,
1061
+ message_id: `result:${delivery.source_agent_id}:${result.turn_id}`,
832
1062
  status: result.status,
833
1063
  elapsed_ms: result.elapsed_ms,
834
1064
  usage: result.usage,
835
1065
  },
836
1066
  };
837
- await this.deliverToRecipient(delivery.destination_agent_id, message);
1067
+ await this.deliverToRecipient(
1068
+ delivery.destination_agent_id,
1069
+ message,
1070
+ () => this.isTerminalDeliveryCurrent(delivery),
1071
+ true,
1072
+ );
838
1073
  });
839
1074
  } catch (error) {
840
- delivery.error = error instanceof Error ? error.message : String(error);
1075
+ if (!this.isTerminalDeliveryCurrent(delivery)) return;
1076
+ const deliveryError = error instanceof Error ? error.message : String(error);
1077
+ this.deliveryLedger = setTerminalDeliveryError(
1078
+ this.deliveryLedger,
1079
+ delivery.source_agent_id,
1080
+ delivery.source_turn_id,
1081
+ deliveryError,
1082
+ );
841
1083
  this.dependencies.registry.append(
842
1084
  createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-settled", {
843
1085
  source_agent_id: delivery.source_agent_id,
844
1086
  source_turn_id: delivery.source_turn_id,
845
- error: delivery.error,
1087
+ error: deliveryError,
846
1088
  }),
847
1089
  );
1090
+ } finally {
1091
+ this.automaticDeliveryKeys.delete(deliveryKey);
848
1092
  }
849
1093
  }
850
1094
 
851
- private async deliverExplicitMessage(
1095
+ private persistCoordinationDelivery(
1096
+ message: CoordinatorMessage,
1097
+ destinationAgentId: string,
1098
+ ): PersistedCoordinationDelivery {
1099
+ const added = addCoordinationDelivery(this.deliveryLedger, {
1100
+ destinationAgentId,
1101
+ message,
1102
+ });
1103
+ this.deliveryLedger = added.ledger;
1104
+ message.details.delivery_id = added.delivery.delivery_id;
1105
+ this.persistCoordinationDeliveryState(added.delivery);
1106
+ return added.delivery;
1107
+ }
1108
+
1109
+ private persistCoordinationDeliveryState(delivery: PersistedCoordinationDelivery): void {
1110
+ this.dependencies.registry.append(
1111
+ createRegistryEvent(
1112
+ this.dependencies.registry.rootSessionId,
1113
+ "coordination-delivery-pending",
1114
+ { delivery },
1115
+ ),
1116
+ );
1117
+ }
1118
+
1119
+ private settleCoordinationDelivery(delivery: PersistedCoordinationDelivery): void {
1120
+ this.deliveryLedger = settleCoordinationDelivery(
1121
+ this.deliveryLedger,
1122
+ delivery.delivery_id,
1123
+ ).ledger;
1124
+ this.waitHandedDeliveryIds.delete(delivery.delivery_id);
1125
+ this.dependencies.registry.append(
1126
+ createRegistryEvent(
1127
+ this.dependencies.registry.rootSessionId,
1128
+ "coordination-delivery-settled",
1129
+ { delivery_id: delivery.delivery_id },
1130
+ ),
1131
+ );
1132
+ this.removeSettledEmptyTurnClaim(
1133
+ delivery.message.details.source_agent_id,
1134
+ delivery.message.details.source_turn_id,
1135
+ );
1136
+ }
1137
+
1138
+ private removeSettledEmptyTurnClaim(sourceAgentId: string, sourceTurnId: string): void {
1139
+ const transition = releaseEmptyDeliveryLedgerTurn(
1140
+ this.deliveryLedger,
1141
+ sourceAgentId,
1142
+ sourceTurnId,
1143
+ this.agents.get(sourceAgentId)?.active_turn_id === sourceTurnId,
1144
+ );
1145
+ this.deliveryLedger = transition.ledger;
1146
+ if (transition.changed) {
1147
+ this.dependencies.registry.append(
1148
+ createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-turn-released", {
1149
+ source_agent_id: sourceAgentId,
1150
+ source_turn_id: sourceTurnId,
1151
+ }),
1152
+ );
1153
+ }
1154
+ }
1155
+
1156
+ private claimDeliveryTurn(sourceAgentId: string, sourceTurnId: string): void {
1157
+ const transition = claimDeliveryLedgerTurn(this.deliveryLedger, sourceAgentId, sourceTurnId);
1158
+ this.deliveryLedger = transition.ledger;
1159
+ if (!transition.changed) return;
1160
+ this.dependencies.registry.append(
1161
+ createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-turn-claimed", {
1162
+ source_agent_id: sourceAgentId,
1163
+ source_turn_id: sourceTurnId,
1164
+ }),
1165
+ );
1166
+ }
1167
+
1168
+ private selectObservableTurnId(
1169
+ sourceAgentId: string,
1170
+ destinationAgentId: string,
1171
+ ): string | undefined {
1172
+ const agent = this.agents.get(sourceAgentId);
1173
+ return selectObservableDeliveryTurn(this.deliveryLedger, {
1174
+ sourceAgentId,
1175
+ destinationAgentId,
1176
+ waitHandedDeliveryIds: this.waitHandedDeliveryIds,
1177
+ activeTurnId: agent?.active_turn_id,
1178
+ latestResultTurnId: agent?.latest_result?.turn_id,
1179
+ });
1180
+ }
1181
+
1182
+ private createExplicitMessage(
852
1183
  callerId: string,
853
1184
  targetId: string,
854
1185
  sourceTurnId: string,
1186
+ messageId: string,
855
1187
  content: string,
856
- ): Promise<void> {
857
- const message: CoordinatorMessage = {
1188
+ ): CoordinatorMessage {
1189
+ return {
858
1190
  customType: "minimal-subagents.message",
859
1191
  content,
860
1192
  details: {
861
1193
  source_agent_id: callerId,
862
1194
  destination_agent_id: targetId,
863
1195
  source_turn_id: sourceTurnId,
1196
+ message_id: messageId,
864
1197
  },
865
1198
  };
1199
+ }
1200
+
1201
+ private recordRecentMessage(targetId: string, message: CoordinatorMessage): void {
866
1202
  if (targetId !== "root") {
867
1203
  const target = this.requireUsableAgent(targetId, "message");
868
- target.recent_messages.push({
869
- source_agent_id: callerId,
870
- turn_id: sourceTurnId,
871
- content,
872
- });
1204
+ const recentMessage = {
1205
+ source_agent_id: message.details.source_agent_id,
1206
+ turn_id: message.details.source_turn_id,
1207
+ content: message.content,
1208
+ };
1209
+ target.recent_messages.push(recentMessage);
873
1210
  if (target.recent_messages.length > RECENT_MESSAGE_LIMIT) target.recent_messages.shift();
1211
+ const recordedAt = this.now().toISOString();
1212
+ target.latest_activity_at = recordedAt;
1213
+ this.dependencies.registry.append(
1214
+ createRegistryEvent(
1215
+ this.dependencies.registry.rootSessionId,
1216
+ "agent-message-recorded",
1217
+ {
1218
+ agent_id: target.agent_id,
1219
+ message: recentMessage,
1220
+ recorded_at: recordedAt,
1221
+ },
1222
+ recordedAt,
1223
+ ),
1224
+ );
1225
+ }
1226
+ }
1227
+
1228
+ private async replayCoordinationDelivery(delivery: PersistedCoordinationDelivery): Promise<void> {
1229
+ if (this.automaticCoordinationDeliveryIds.has(delivery.delivery_id)) return;
1230
+ this.automaticCoordinationDeliveryIds.add(delivery.delivery_id);
1231
+ try {
1232
+ await this.enqueueRecipientDelivery(delivery.destination_agent_id, async () => {
1233
+ if (this.hasCoordinationDeliveryEvidence(delivery)) {
1234
+ this.settleCoordinationDelivery(delivery);
1235
+ return;
1236
+ }
1237
+ this.waitHandedDeliveryIds.add(delivery.delivery_id);
1238
+ await this.deliverToRecipient(delivery.destination_agent_id, delivery.message, () =>
1239
+ this.isCoordinationDeliveryCurrent(delivery),
1240
+ );
1241
+ });
1242
+ } catch (error) {
1243
+ this.waitHandedDeliveryIds.delete(delivery.delivery_id);
1244
+ if (!this.isCoordinationDeliveryCurrent(delivery)) return;
1245
+ const deliveryError = error instanceof Error ? error.message : String(error);
1246
+ this.deliveryLedger = setCoordinationDeliveryError(
1247
+ this.deliveryLedger,
1248
+ delivery.delivery_id,
1249
+ deliveryError,
1250
+ );
1251
+ const current = findCoordinationDelivery(this.deliveryLedger, delivery.delivery_id);
1252
+ if (current) this.persistCoordinationDeliveryState(current);
1253
+ } finally {
1254
+ this.automaticCoordinationDeliveryIds.delete(delivery.delivery_id);
1255
+ }
1256
+ }
1257
+
1258
+ private async deliverExplicitMessage(
1259
+ message: CoordinatorMessage,
1260
+ targetId: string,
1261
+ delivery: PersistedCoordinationDelivery,
1262
+ ): Promise<AgentMessageDisposition> {
1263
+ this.waitHandedDeliveryIds.add(delivery.delivery_id);
1264
+ try {
1265
+ await this.deliverToRecipient(targetId, message, () =>
1266
+ this.isCoordinationDeliveryCurrent(delivery),
1267
+ );
1268
+ return "queued";
1269
+ } catch (error) {
1270
+ this.waitHandedDeliveryIds.delete(delivery.delivery_id);
1271
+ throw error;
874
1272
  }
875
- await this.deliverToRecipient(targetId, message);
876
1273
  }
877
1274
 
878
- private async deliverToRecipient(targetId: string, message: CoordinatorMessage): Promise<void> {
1275
+ private async deliverToRecipient(
1276
+ targetId: string,
1277
+ message: CoordinatorMessage,
1278
+ isCurrentDelivery: () => boolean = () => true,
1279
+ requireIdleRecipient = false,
1280
+ ): Promise<void> {
1281
+ if (!isCurrentDelivery()) {
1282
+ throw new Error("Minimal subagents delivery abandoned after session branch change");
1283
+ }
879
1284
  if (!this.acceptingOperations) {
880
1285
  throw new Error("Minimal subagents delivery stopped during coordinator shutdown");
881
1286
  }
882
1287
  if (targetId === "root") {
883
- await this.dependencies.root.steerCoordinatorMessage(message);
1288
+ await this.dependencies.root.queueCoordinatorMessage(addCoordinatorMessageEnvelope(message));
884
1289
  return;
885
1290
  }
886
1291
  const target = this.requireUsableAgent(targetId, "message");
887
- const runtime = await this.ensureRuntime(target);
1292
+ const runtime = this.runtimes.get(targetId) ?? (await this.ensureRuntime(target));
1293
+ if (!isCurrentDelivery()) {
1294
+ throw new Error("Minimal subagents delivery abandoned after session branch change");
1295
+ }
1296
+ 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
+ }
888
1300
  if (target.active_turn_id || runtime.isRunning) {
889
- await runtime.steerCoordinatorMessage(message);
1301
+ await runtime.queueCoordinatorMessage(visibleMessage);
890
1302
  return;
891
1303
  }
892
1304
  const turnId = this.beginTurn(target);
893
1305
  const runMessage = runtime
894
- .runMessage(message)
1306
+ .runMessage(visibleMessage)
895
1307
  .then((outcome) => {
896
- if (target.active_turn_id === turnId) {
1308
+ if (this.agents.get(target.agent_id) === target && target.active_turn_id === turnId) {
897
1309
  this.settleTurn(target, turnId, terminalTurnResult(target.agent_id, turnId, outcome));
898
1310
  }
899
1311
  })
900
- .catch((error: unknown) => {
901
- if (target.active_turn_id === turnId) {
1312
+ .catch((cause) => {
1313
+ if (this.agents.get(target.agent_id) === target && target.active_turn_id === turnId) {
902
1314
  this.settleTurn(target, turnId, {
903
1315
  agent_id: target.agent_id,
904
1316
  turn_id: turnId,
905
1317
  status: "failed",
906
1318
  output: "",
907
- error: error instanceof Error ? error.message : String(error),
1319
+ error: cause instanceof Error ? cause.message : String(cause),
908
1320
  });
909
1321
  }
910
1322
  });
@@ -917,12 +1329,17 @@ export class MinimalSubagentsCoordinator {
917
1329
  void operation.then(cleanup, cleanup);
918
1330
  }
919
1331
 
920
- private enqueueRecipientDelivery(
921
- targetId: string,
922
- operation: () => Promise<void>,
923
- ): Promise<void> {
1332
+ private enqueueRecipientDelivery<T>(targetId: string, operation: () => Promise<T>): Promise<T> {
1333
+ const epoch = this.lifecycleEpoch;
924
1334
  const previous = this.recipientQueues.get(targetId) ?? Promise.resolve();
925
- const next = previous.catch(() => undefined).then(operation);
1335
+ const next = previous
1336
+ .catch(() => undefined)
1337
+ .then(() => {
1338
+ if (epoch !== this.lifecycleEpoch) {
1339
+ throw new Error("Minimal subagents delivery abandoned after session branch change");
1340
+ }
1341
+ return operation();
1342
+ });
926
1343
  this.recipientQueues.set(targetId, next);
927
1344
  const cleanup = () => {
928
1345
  if (this.recipientQueues.get(targetId) === next) this.recipientQueues.delete(targetId);
@@ -960,6 +1377,46 @@ export class MinimalSubagentsCoordinator {
960
1377
  return targetId;
961
1378
  }
962
1379
 
1380
+ private applyDeliveryLedgerTransition(transition: DeliveryLedgerTransition): void {
1381
+ this.deliveryLedger = transition.ledger;
1382
+ for (const delivery of transition.prunedTerminalDeliveries) {
1383
+ const key = agentDeliveryKey(delivery.source_agent_id, delivery.source_turn_id);
1384
+ this.releaseAutomaticDeliveryClaimWaiters(key);
1385
+ this.dependencies.registry.append(
1386
+ createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pruned", {
1387
+ source_agent_id: delivery.source_agent_id,
1388
+ source_turn_id: delivery.source_turn_id,
1389
+ reason: "retention-limit",
1390
+ }),
1391
+ );
1392
+ }
1393
+ }
1394
+
1395
+ private isTerminalDeliveryCurrent(delivery: PersistedDelivery): boolean {
1396
+ const current = findTerminalDelivery(
1397
+ this.deliveryLedger,
1398
+ delivery.source_agent_id,
1399
+ delivery.source_turn_id,
1400
+ );
1401
+ return current?.sequence === delivery.sequence;
1402
+ }
1403
+
1404
+ private shouldStopAutomaticTerminalDelivery(delivery: PersistedDelivery): boolean {
1405
+ const current = findTerminalDelivery(
1406
+ this.deliveryLedger,
1407
+ delivery.source_agent_id,
1408
+ delivery.source_turn_id,
1409
+ );
1410
+ return current === undefined || current.path === "wait";
1411
+ }
1412
+
1413
+ private isCoordinationDeliveryCurrent(delivery: PersistedCoordinationDelivery): boolean {
1414
+ return (
1415
+ findCoordinationDelivery(this.deliveryLedger, delivery.delivery_id)?.sequence ===
1416
+ delivery.sequence
1417
+ );
1418
+ }
1419
+
963
1420
  private hasDeliveryEvidence(delivery: PersistedDelivery): boolean {
964
1421
  if (delivery.destination_agent_id === "root") {
965
1422
  return this.dependencies.root.hasDeliveryEvidence(
@@ -974,15 +1431,36 @@ export class MinimalSubagentsCoordinator {
974
1431
  );
975
1432
  }
976
1433
 
1434
+ private hasCoordinationDeliveryEvidence(delivery: PersistedCoordinationDelivery): boolean {
1435
+ const sourceAgentId = delivery.message.details.source_agent_id;
1436
+ const sourceTurnId = delivery.message.details.source_turn_id;
1437
+ if (delivery.destination_agent_id === "root") {
1438
+ return this.dependencies.root.hasDeliveryEvidence(
1439
+ sourceAgentId,
1440
+ sourceTurnId,
1441
+ delivery.delivery_id,
1442
+ );
1443
+ }
1444
+ return (
1445
+ this.runtimes
1446
+ .get(delivery.destination_agent_id)
1447
+ ?.hasDeliveryEvidence(sourceAgentId, sourceTurnId, delivery.delivery_id) ?? false
1448
+ );
1449
+ }
1450
+
977
1451
  private settleDelivery(delivery: PersistedDelivery): void {
978
- delivery.settled = true;
979
- delivery.error = undefined;
1452
+ this.deliveryLedger = settleTerminalDelivery(
1453
+ this.deliveryLedger,
1454
+ delivery.source_agent_id,
1455
+ delivery.source_turn_id,
1456
+ ).ledger;
980
1457
  this.dependencies.registry.append(
981
1458
  createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-settled", {
982
1459
  source_agent_id: delivery.source_agent_id,
983
1460
  source_turn_id: delivery.source_turn_id,
984
1461
  }),
985
1462
  );
1463
+ this.removeSettledEmptyTurnClaim(delivery.source_agent_id, delivery.source_turn_id);
986
1464
  }
987
1465
 
988
1466
  private buildAgentSummary(agent: PersistedAgent, includeDescendants = true): AgentSummary {
@@ -993,6 +1471,10 @@ export class MinimalSubagentsCoordinator {
993
1471
  const elapsed = agent.active_turn_started_at
994
1472
  ? Math.max(0, this.now().getTime() - new Date(agent.active_turn_started_at).getTime())
995
1473
  : undefined;
1474
+ const runtimeProfile = this.runtimes.get(agent.agent_id)?.getRuntimeProfile() ?? {
1475
+ model: agent.launch_contract.model,
1476
+ thinking_level: agent.launch_contract.thinking_level,
1477
+ };
996
1478
  return {
997
1479
  agent_id: agent.agent_id,
998
1480
  parent_id: agent.parent_id,
@@ -1002,8 +1484,7 @@ export class MinimalSubagentsCoordinator {
1002
1484
  latest_turn: agent.latest_result
1003
1485
  ? { turn_id: agent.latest_result.turn_id, status: agent.latest_result.status }
1004
1486
  : undefined,
1005
- model: agent.launch_contract.model,
1006
- thinking_level: agent.launch_contract.thinking_level,
1487
+ ...runtimeProfile,
1007
1488
  tools: [...agent.launch_contract.ordinary_tools],
1008
1489
  elapsed_ms: elapsed ?? agent.latest_result?.elapsed_ms,
1009
1490
  latest_activity_at: agent.latest_activity_at ?? agent.created_at,
@@ -1024,7 +1505,7 @@ export class MinimalSubagentsCoordinator {
1024
1505
  return {
1025
1506
  ...summary,
1026
1507
  session_file: agent.session_file,
1027
- launch_contract: structuredClone(agent.launch_contract) as unknown as Record<string, unknown>,
1508
+ launch_contract: structuredClone(agent.launch_contract),
1028
1509
  capability_ceiling: [...agent.capability_ceiling],
1029
1510
  spawn_entry_id: agent.spawn_entry_id,
1030
1511
  recent_messages: structuredClone(agent.recent_messages),
@@ -1035,6 +1516,47 @@ export class MinimalSubagentsCoordinator {
1035
1516
  };
1036
1517
  }
1037
1518
 
1519
+ private pruneDeliveryStateForDeletedAgent(agentId: string): void {
1520
+ const previousTerminalDeliveries = this.deliveryLedger.terminalDeliveries;
1521
+ const previousCoordinationDeliveries = this.deliveryLedger.coordinationDeliveries;
1522
+ 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
+ for (const delivery of previousCoordinationDeliveries) {
1531
+ if (!this.isCoordinationDeliveryCurrent(delivery)) {
1532
+ this.waitHandedDeliveryIds.delete(delivery.delivery_id);
1533
+ }
1534
+ }
1535
+ for (const [key, messages] of this.pendingParentMessages) {
1536
+ for (const pending of messages) {
1537
+ if (!findCoordinationDelivery(this.deliveryLedger, pending.deliveryId)) {
1538
+ pending.claimed = true;
1539
+ pending.releaseClaim();
1540
+ }
1541
+ }
1542
+ const retained = messages.filter((pending) =>
1543
+ findCoordinationDelivery(this.deliveryLedger, pending.deliveryId),
1544
+ );
1545
+ if (retained.length === 0) this.pendingParentMessages.delete(key);
1546
+ else this.pendingParentMessages.set(key, retained);
1547
+ }
1548
+ }
1549
+
1550
+ private pruneRecentMessageProjectionsForDeletedAgent(agentId: string): void {
1551
+ const belongsToDeletedSubtree = (sourceAgentId: string) =>
1552
+ sourceAgentId === agentId || sourceAgentId.startsWith(`${agentId}.`);
1553
+ for (const agent of this.agents.values()) {
1554
+ agent.recent_messages = agent.recent_messages.filter(
1555
+ (message) => !belongsToDeletedSubtree(message.source_agent_id),
1556
+ );
1557
+ }
1558
+ }
1559
+
1038
1560
  private descendantsOf(agentId: string): PersistedAgent[] {
1039
1561
  const descendants: PersistedAgent[] = [];
1040
1562
  const queue = this.childrenOf(agentId);
@@ -1122,6 +1644,7 @@ export class MinimalSubagentsCoordinator {
1122
1644
  ...structuredClone(agent),
1123
1645
  session_file: undefined,
1124
1646
  session_id: undefined,
1647
+ session_leaf_id: undefined,
1125
1648
  clone_error: cloneError,
1126
1649
  active_turn_id: undefined,
1127
1650
  active_turn_started_at: undefined,
@@ -1132,6 +1655,15 @@ export class MinimalSubagentsCoordinator {
1132
1655
  };
1133
1656
  }
1134
1657
 
1658
+ private rejectAllWaiters(error: Error): void {
1659
+ for (const [key, waiters] of this.waiters) {
1660
+ for (const waiter of waiters) {
1661
+ this.removeWaiter(key, waiter);
1662
+ waiter.reject(error);
1663
+ }
1664
+ }
1665
+ }
1666
+
1135
1667
  private removeWaiter(key: string, waiter: TurnWaiter): void {
1136
1668
  const turnWaiters = this.waiters.get(key);
1137
1669
  turnWaiters?.delete(waiter);
@@ -1142,6 +1674,295 @@ export class MinimalSubagentsCoordinator {
1142
1674
  }
1143
1675
  }
1144
1676
 
1677
+ private resolveWaiterWithMessage(
1678
+ message: CoordinatorMessage,
1679
+ delivery: PersistedCoordinationDelivery,
1680
+ ): boolean {
1681
+ const destinationAgentId = message.details.destination_agent_id;
1682
+ if (!destinationAgentId) return false;
1683
+ const key = agentDeliveryKey(message.details.source_agent_id, message.details.source_turn_id);
1684
+ const turnWaiters = this.waiters.get(key);
1685
+ const waiter = [...(turnWaiters ?? [])].find(
1686
+ (candidate) => candidate.callerId === destinationAgentId,
1687
+ );
1688
+ if (!waiter) return false;
1689
+ this.claimDeliveryTurn(message.details.source_agent_id, message.details.source_turn_id);
1690
+ this.deliveryLedger = setCoordinationDeliveryPath(
1691
+ this.deliveryLedger,
1692
+ delivery.delivery_id,
1693
+ "wait",
1694
+ );
1695
+ const currentDelivery = findCoordinationDelivery(this.deliveryLedger, delivery.delivery_id);
1696
+ if (currentDelivery) this.persistCoordinationDeliveryState(currentDelivery);
1697
+ this.waitHandedDeliveryIds.add(delivery.delivery_id);
1698
+ this.removeWaiter(key, waiter);
1699
+ waiter.resolve({
1700
+ event: "message",
1701
+ agent_id: message.details.source_agent_id,
1702
+ turn_id: message.details.source_turn_id,
1703
+ message_id: message.details.message_id,
1704
+ delivery_id: delivery.delivery_id,
1705
+ message: message.content,
1706
+ });
1707
+ return true;
1708
+ }
1709
+
1710
+ private claimPendingParentMessage(
1711
+ callerId: string,
1712
+ sourceAgentId: string,
1713
+ sourceTurnId: string,
1714
+ ): WaitResult | undefined {
1715
+ const key = agentDeliveryKey(sourceAgentId, sourceTurnId);
1716
+ const pendingMessages = this.pendingParentMessages.get(key);
1717
+ const index = pendingMessages?.findIndex(
1718
+ (pending) => pending.destinationAgentId === callerId && !pending.claimed,
1719
+ );
1720
+ if (index === undefined || index < 0 || !pendingMessages) {
1721
+ const retained = this.deliveryLedger.coordinationDeliveries
1722
+ .filter(
1723
+ (delivery) =>
1724
+ delivery.destination_agent_id === callerId &&
1725
+ delivery.message.details.source_agent_id === sourceAgentId &&
1726
+ delivery.message.details.source_turn_id === sourceTurnId &&
1727
+ !this.waitHandedDeliveryIds.has(delivery.delivery_id),
1728
+ )
1729
+ .sort((left, right) => left.sequence - right.sequence)[0];
1730
+ if (!retained) return undefined;
1731
+ this.claimDeliveryTurn(sourceAgentId, sourceTurnId);
1732
+ this.deliveryLedger = setCoordinationDeliveryPath(
1733
+ this.deliveryLedger,
1734
+ retained.delivery_id,
1735
+ "wait",
1736
+ );
1737
+ const currentDelivery = findCoordinationDelivery(this.deliveryLedger, retained.delivery_id);
1738
+ if (currentDelivery) this.persistCoordinationDeliveryState(currentDelivery);
1739
+ this.waitHandedDeliveryIds.add(retained.delivery_id);
1740
+ return {
1741
+ event: "message",
1742
+ agent_id: sourceAgentId,
1743
+ turn_id: sourceTurnId,
1744
+ message_id: retained.message.details.message_id,
1745
+ delivery_id: retained.delivery_id,
1746
+ message: retained.message.content,
1747
+ };
1748
+ }
1749
+ const pending = pendingMessages[index];
1750
+ if (!pending) return undefined;
1751
+ this.claimDeliveryTurn(sourceAgentId, sourceTurnId);
1752
+ const delivery = findCoordinationDelivery(this.deliveryLedger, pending.deliveryId);
1753
+ if (delivery) {
1754
+ this.deliveryLedger = setCoordinationDeliveryPath(
1755
+ this.deliveryLedger,
1756
+ delivery.delivery_id,
1757
+ "wait",
1758
+ );
1759
+ const currentDelivery = findCoordinationDelivery(this.deliveryLedger, delivery.delivery_id);
1760
+ if (currentDelivery) this.persistCoordinationDeliveryState(currentDelivery);
1761
+ this.waitHandedDeliveryIds.add(delivery.delivery_id);
1762
+ }
1763
+ const sourceResult = this.agents.get(sourceAgentId)?.latest_result;
1764
+ if (sourceResult?.turn_id === sourceTurnId) {
1765
+ this.setTerminalDeliveryPathToWait(callerId, sourceResult);
1766
+ }
1767
+ pending.claimed = true;
1768
+ pending.cancelGrace?.();
1769
+ pending.releaseClaim();
1770
+ pendingMessages.splice(index, 1);
1771
+ if (pendingMessages.length === 0) this.pendingParentMessages.delete(key);
1772
+ return {
1773
+ event: "message",
1774
+ agent_id: sourceAgentId,
1775
+ turn_id: sourceTurnId,
1776
+ message_id: pending.message.details.message_id,
1777
+ delivery_id: pending.deliveryId,
1778
+ message: pending.message.content,
1779
+ };
1780
+ }
1781
+
1782
+ private queuePendingParentMessage(
1783
+ targetId: string,
1784
+ message: CoordinatorMessage,
1785
+ delivery: PersistedCoordinationDelivery,
1786
+ ): void {
1787
+ const key = agentDeliveryKey(message.details.source_agent_id, message.details.source_turn_id);
1788
+ const turnClaimed = () =>
1789
+ isDeliveryLedgerTurnClaimed(
1790
+ this.deliveryLedger,
1791
+ message.details.source_agent_id,
1792
+ message.details.source_turn_id,
1793
+ );
1794
+ let releaseClaim!: () => void;
1795
+ const claimPromise = new Promise<void>((resolve) => {
1796
+ releaseClaim = resolve;
1797
+ });
1798
+ const pending: PendingParentMessage = {
1799
+ deliveryId: delivery.delivery_id,
1800
+ message,
1801
+ destinationAgentId: targetId,
1802
+ claimed: false,
1803
+ claimPromise,
1804
+ releaseClaim,
1805
+ };
1806
+ const pendingMessages = this.pendingParentMessages.get(key) ?? [];
1807
+ pendingMessages.push(pending);
1808
+ this.pendingParentMessages.set(key, pendingMessages);
1809
+
1810
+ if (
1811
+ isDeliveryLedgerTurnClaimed(
1812
+ this.deliveryLedger,
1813
+ message.details.source_agent_id,
1814
+ message.details.source_turn_id,
1815
+ )
1816
+ )
1817
+ return;
1818
+
1819
+ const operation = this.enqueueRecipientDelivery(targetId, async () => {
1820
+ if (targetId !== "root") {
1821
+ await this.ensureRuntime(this.requireUsableAgent(targetId, "message"));
1822
+ if (!this.isCoordinationDeliveryCurrent(delivery)) return;
1823
+ }
1824
+ const graceMs = this.deliveryGraceMs();
1825
+ const gracePromise = new Promise<void>((resolve) => {
1826
+ if (graceMs <= 0) {
1827
+ resolve();
1828
+ return;
1829
+ }
1830
+ const timer = setTimeout(resolve, graceMs);
1831
+ pending.cancelGrace = () => {
1832
+ clearTimeout(timer);
1833
+ resolve();
1834
+ };
1835
+ });
1836
+ await Promise.race([pending.claimPromise, gracePromise]);
1837
+ pending.cancelGrace?.();
1838
+ pending.cancelGrace = undefined;
1839
+ 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
+ if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
1847
+ this.removePendingParentMessage(key, pending);
1848
+ this.waitHandedDeliveryIds.add(delivery.delivery_id);
1849
+ await this.deliverToRecipient(
1850
+ targetId,
1851
+ message,
1852
+ () => this.isCoordinationDeliveryCurrent(delivery),
1853
+ true,
1854
+ );
1855
+ });
1856
+ void operation.catch((cause) => {
1857
+ this.removePendingParentMessage(key, pending);
1858
+ this.waitHandedDeliveryIds.delete(delivery.delivery_id);
1859
+ if (!this.isCoordinationDeliveryCurrent(delivery)) return;
1860
+ const deliveryError = cause instanceof Error ? cause.message : String(cause);
1861
+ this.deliveryLedger = setCoordinationDeliveryError(
1862
+ this.deliveryLedger,
1863
+ delivery.delivery_id,
1864
+ deliveryError,
1865
+ );
1866
+ const current = findCoordinationDelivery(this.deliveryLedger, delivery.delivery_id);
1867
+ if (current) this.persistCoordinationDeliveryState(current);
1868
+ this.dependencies.notify?.({
1869
+ type: "failure",
1870
+ agentId: message.details.source_agent_id,
1871
+ message: `Could not queue message ${message.details.message_id}: ${
1872
+ cause instanceof Error ? cause.message : String(cause)
1873
+ }`,
1874
+ });
1875
+ });
1876
+ }
1877
+
1878
+ private removePendingParentMessage(key: string, pending: PendingParentMessage): void {
1879
+ const pendingMessages = this.pendingParentMessages.get(key);
1880
+ if (!pendingMessages) return;
1881
+ const index = pendingMessages.indexOf(pending);
1882
+ if (index >= 0) pendingMessages.splice(index, 1);
1883
+ if (pendingMessages.length === 0) this.pendingParentMessages.delete(key);
1884
+ }
1885
+
1886
+ private claimTerminalDelivery(callerId: string, result: TurnResult): void {
1887
+ if (!findTerminalDelivery(this.deliveryLedger, result.agent_id, result.turn_id)) return;
1888
+ this.claimDeliveryTurn(result.agent_id, result.turn_id);
1889
+ this.setTerminalDeliveryPathToWait(callerId, result);
1890
+ }
1891
+
1892
+ private setTerminalDeliveryPathToWait(callerId: string, result: TurnResult): void {
1893
+ const key = agentDeliveryKey(result.agent_id, result.turn_id);
1894
+ const delivery = findTerminalDelivery(this.deliveryLedger, result.agent_id, result.turn_id);
1895
+ if (!delivery || delivery.destination_agent_id !== callerId || delivery.path === "wait") return;
1896
+ this.applyDeliveryLedgerTransition(
1897
+ setTerminalDeliveryPath(this.deliveryLedger, result.agent_id, result.turn_id, "wait"),
1898
+ );
1899
+ const retained = findTerminalDelivery(this.deliveryLedger, result.agent_id, result.turn_id);
1900
+ this.releaseAutomaticDeliveryClaimWaiters(key);
1901
+ if (!retained) return;
1902
+ this.dependencies.registry.append(
1903
+ createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pending", {
1904
+ delivery: retained,
1905
+ }),
1906
+ );
1907
+ }
1908
+
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
+
1145
1966
  private async cancelDuringShutdown(agentId: string): Promise<void> {
1146
1967
  const target = this.agents.get(agentId);
1147
1968
  if (!target) return;
@@ -1151,6 +1972,7 @@ export class MinimalSubagentsCoordinator {
1151
1972
  const turnId = agent.active_turn_id;
1152
1973
  const runtime = this.runtimes.get(agent.agent_id);
1153
1974
  if (runtime) await runtime.abort();
1975
+ if (this.agents.get(agent.agent_id) !== agent) continue;
1154
1976
  this.settleTurn(agent, turnId, {
1155
1977
  agent_id: agent.agent_id,
1156
1978
  turn_id: turnId,
@@ -1165,6 +1987,7 @@ export class MinimalSubagentsCoordinator {
1165
1987
  const turnId = agent.active_turn_id;
1166
1988
  const runtime = this.runtimes.get(agent.agent_id);
1167
1989
  if (runtime) await runtime.abort();
1990
+ if (this.agents.get(agent.agent_id) !== agent) return undefined;
1168
1991
  this.settleTurn(agent, turnId, {
1169
1992
  agent_id: agent.agent_id,
1170
1993
  turn_id: turnId,
@@ -1227,4 +2050,8 @@ export class MinimalSubagentsCoordinator {
1227
2050
  private now(): Date {
1228
2051
  return this.dependencies.now?.() ?? new Date();
1229
2052
  }
2053
+
2054
+ private deliveryGraceMs(): number {
2055
+ return this.dependencies.automaticDeliveryGraceMs ?? DEFAULT_AUTOMATIC_DELIVERY_GRACE_MS;
2056
+ }
1230
2057
  }