pi-extended-teams 2.2.9 → 2.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.
Files changed (54) hide show
  1. package/README.md +182 -2
  2. package/extensions/agents/read-agent-report.ts +11 -2
  3. package/extensions/agents/read-agent-session-lifecycle.ts +25 -10
  4. package/extensions/agents/read-agent.ts +387 -65
  5. package/extensions/agents/write-agent.ts +3 -0
  6. package/extensions/events/register-events.ts +22 -10
  7. package/extensions/index.ts +132 -38
  8. package/extensions/internal/pi-check-operations.ts +24 -0
  9. package/extensions/internal/session-files.ts +10 -2
  10. package/extensions/runtime/teammate-interrupt.ts +11 -5
  11. package/extensions/runtime/types.ts +4 -0
  12. package/extensions/team/lifecycle.ts +26 -0
  13. package/extensions/team/session-cost.ts +155 -0
  14. package/extensions/tools/agent-communication-tools.ts +24 -8
  15. package/extensions/tools/agent-status-tool.ts +50 -7
  16. package/extensions/tools/coordination-tools.ts +78 -8
  17. package/extensions/tools/task-runtime-tools.ts +23 -4
  18. package/extensions/tools/team-tools.ts +287 -40
  19. package/extensions/ui/activity-colors.ts +59 -0
  20. package/extensions/ui/agent-follow-view.ts +22 -12
  21. package/extensions/ui/agent-navigation.ts +35 -1
  22. package/extensions/ui/checkpoints-command.ts +28 -0
  23. package/extensions/ui/extensions-command.ts +5 -1
  24. package/extensions/ui/onboarding-command.ts +218 -0
  25. package/extensions/ui/status-widget.ts +22 -14
  26. package/package.json +1 -1
  27. package/skills/teams.md +16 -0
  28. package/src/orchestration/index.ts +19 -1
  29. package/src/orchestration/status-projection.ts +2 -3
  30. package/src/orchestration/types.ts +9 -1
  31. package/src/results/check-journal.ts +149 -0
  32. package/src/results/check-policy.ts +86 -0
  33. package/src/results/check-runner.ts +110 -0
  34. package/src/results/checkpoint-assignment.ts +60 -0
  35. package/src/results/checkpoint-policy.ts +20 -0
  36. package/src/results/checkpoint-report.ts +109 -0
  37. package/src/results/checkpoint-retention.ts +15 -0
  38. package/src/results/completion-group-delivery.ts +66 -0
  39. package/src/results/completion-group-recovery.ts +61 -0
  40. package/src/results/completion-group-wake.ts +62 -0
  41. package/src/results/completion-group.ts +290 -0
  42. package/src/results/continuation-recipient.ts +37 -0
  43. package/src/results/durable-json.ts +29 -0
  44. package/src/results/repair-policy.ts +24 -0
  45. package/src/results/report-result.ts +77 -0
  46. package/src/results/source-identity.ts +115 -0
  47. package/src/results/specialist-checkpoint.ts +227 -0
  48. package/src/results/verification-controller.ts +308 -0
  49. package/src/utils/messaging.ts +47 -10
  50. package/src/utils/models.ts +13 -0
  51. package/src/utils/paths.ts +4 -0
  52. package/src/utils/report-events.ts +95 -10
  53. package/src/utils/teams.ts +3 -1
  54. package/src/utils/write-queue.ts +3 -0
@@ -8,10 +8,18 @@ import * as runtime from "../../src/utils/runtime";
8
8
  import * as teams from "../../src/utils/teams";
9
9
  import * as messaging from "../../src/utils/messaging";
10
10
  import * as reportEvents from "../../src/utils/report-events";
11
- import type { Member } from "../../src/utils/models";
11
+ import { checkpointReference, preflightReportCheckpoint, saveReportCheckpoint } from "../../src/results/checkpoint-report";
12
+ import type { Member, TeamReportEvent } from "../../src/utils/models";
13
+ import { deliverCompletionGroupReport } from "../../src/results/completion-group-delivery";
14
+ import { createReportResult, effectiveTaskOutcome, normalizeReportedTaskDetails, type ReportResult, type ReportedTaskDetails } from "../../src/results/report-result";
15
+ import { assignedCheckIds, normalizeCheckPolicy, type CheckDefinition } from "../../src/results/check-policy";
16
+ import { normalizeRepairPolicy, type RepairPolicy } from "../../src/results/repair-policy";
17
+ import { VerificationController, type VerificationDecision } from "../../src/results/verification-controller";
18
+ import type { CheckRunnerOptions } from "../../src/results/check-runner";
19
+ import { loadNativeCheckOperations } from "../internal/pi-check-operations";
12
20
  import type { AgentReportSource, CompletedAgentReport, RunningReadAgent } from "../runtime/types";
13
21
  import { extractTextParts, sanitizeTuiLine } from "../ui/renderers";
14
- import { createAgentCommunicationTools, type SubmittedAgentReport } from "../tools/agent-communication-tools";
22
+ import { createAgentCommunicationTools, formatRepairRequest, type SubmittedAgentReport } from "../tools/agent-communication-tools";
15
23
  import { requireWriteAgentTeam } from "../team/roster";
16
24
  import { isPiPromptPlanningMember, shouldSuppressLeadReportInjection } from "../../src/utils/workflow-metadata";
17
25
  import { canonicalPersistedModelSlot, loadSettings, requireFavoriteModelLevel } from "../../src/utils/settings";
@@ -19,6 +27,7 @@ import { parseQualifiedModel } from "../../src/utils/model-resolution";
19
27
  import { closePersistedRecipient } from "../team/recipient-closure";
20
28
  import { generateExtensionInstanceId, generateLifecycleRunId, withLifecycleTombstoneLock } from "../../src/utils/lifecycle-tombstone";
21
29
  import { createLifecycleRuntime, type ShutdownTeammateOptions } from "../team/lifecycle";
30
+ import { recordedSessionCost, type AgentCostSnapshot, type CostOutcome, type CostRun } from "../team/session-cost";
22
31
  import {
23
32
  createSpawnResourcePlan,
24
33
  parentProjectTrustForSpawn,
@@ -27,12 +36,13 @@ import {
27
36
  import { loadPiRuntimeApi } from "../internal/pi-runtime-api";
28
37
  import { preparePrivateAgentSessionDirectory } from "../internal/agent-session-files";
29
38
  import { isEligibleNestedReadParent, NESTED_DELEGATION_TOOL_NAMES } from "../runtime/nested-read-agents";
30
- import type { NestedReadAgentToolBinding } from "../tools/team-tools";
39
+ import { CHILD_AGENT_LIFECYCLE_PROBE, type NestedReadAgentToolBinding } from "../tools/team-tools";
31
40
  import type { ParentRunIdentity, PendingChildController } from "../runtime/pending-child-controller";
32
41
  import {
33
42
  closeReadAgentMessageDelivery,
34
43
  enqueueReadAgentMessageDelivery,
35
44
  installReadAgentSessionLifecycle,
45
+ ReadAgentDeliveryCancelledError,
36
46
  type ReadAgentDeliveryCloseResult,
37
47
  type ReadAgentTeardownResult,
38
48
  } from "./read-agent-session-lifecycle";
@@ -50,6 +60,7 @@ import {
50
60
  export { closeReadAgentMessageDelivery } from "./read-agent-session-lifecycle";
51
61
 
52
62
  const pendingParentWakeSignals = new WeakMap<RunningReadAgent, () => void>();
63
+ const repairCancellationHandlers = new WeakMap<RunningReadAgent, () => Promise<void>>();
53
64
 
54
65
  interface ReadAgentWakeState {
55
66
  generation: number;
@@ -114,12 +125,16 @@ export interface RunReadAgentOptions {
114
125
  teamName: string,
115
126
  recipient: string,
116
127
  content: string,
117
- expectedRecipientRunId?: string
128
+ expectedRecipientRunId?: string,
129
+ requireReceipt?: boolean
118
130
  ): Promise<boolean>;
119
131
  createResourcePlan?(input: { cwd: string; projectTrusted: boolean }): SpawnResourcePlan | Promise<SpawnResourcePlan>;
120
132
  extensionInstanceId?: string;
121
133
  createNestedReadAgentTools?(binding: NestedReadAgentToolBinding): any[];
134
+ nestedChildSnapshot?(binding: NestedReadAgentToolBinding): { running: number; queued: number };
122
135
  pendingChildController?: PendingChildController;
136
+ loadCheckOperations?: CheckRunnerOptions["loadOperations"];
137
+ beginCostRun?(rootSessionId: string, teamName: string, lifecycleRunId: string): CostRun;
123
138
  }
124
139
 
125
140
  function pushReadAgentEvent(agent: RunningReadAgent, text: string): void {
@@ -165,8 +180,9 @@ async function runReadAgentSessionOperation(
165
180
  failure = error;
166
181
  }
167
182
 
168
- if (state.activeOperationGeneration === generation) state.activeOperationGeneration = undefined;
169
183
  const interrupted = state.interruptRequestedGeneration === generation;
184
+ if (interrupted || failure || state.stopRequested) await repairCancellationHandlers.get(state)?.();
185
+ if (state.activeOperationGeneration === generation) state.activeOperationGeneration = undefined;
170
186
  if (interrupted) state.interruptRequestedGeneration = undefined;
171
187
  state.completedOperationGeneration = generation;
172
188
  state.completedOperationInterrupted = interrupted;
@@ -349,30 +365,68 @@ export function handleReadAgentSessionEvent(
349
365
  }
350
366
  }
351
367
 
352
- export async function sendMessageToRunningReadAgent(agent: RunningReadAgent | undefined, content: string): Promise<boolean> {
368
+ export async function sendMessageToRunningReadAgent(
369
+ agent: RunningReadAgent | undefined,
370
+ content: string,
371
+ requireReceipt = false
372
+ ): Promise<boolean> {
353
373
  if (!agent) return false;
354
374
  if (!agent.session || !agent.acceptingMessages || agent.messageDeliveryClosed || agent.stopRequested) {
355
375
  throw new Error(`Cannot send message to ${agent.name}: agent is finishing.`);
356
376
  }
357
377
 
358
378
  const session = agent.session;
379
+ let received = false;
380
+ let acknowledge!: () => void;
381
+ const receipt = new Promise<void>(resolve => { acknowledge = resolve; });
382
+ let unsubscribe: (() => void) | undefined;
359
383
  const deliveryResult = enqueueReadAgentMessageDelivery(
360
384
  agent,
361
385
  agent.name,
362
386
  async () => {
387
+ const check = agent.checkOperation;
388
+ if (check || !session.isStreaming) await (agent.activeOperationSettlementPromise ?? check?.settled);
363
389
  const pendingInterrupt = agent.operationInterruptPromise;
364
390
  if (pendingInterrupt) await pendingInterrupt.catch(() => {});
391
+ if (agent.messageDeliveryClosed || agent.stopRequested) throw new ReadAgentDeliveryCancelledError(agent.name);
392
+ if (requireReceipt) {
393
+ unsubscribe = session.subscribe(event => {
394
+ if (event.type !== "message_end" || event.message.role !== "user") return;
395
+ const body = event.message.content;
396
+ const text = typeof body === "string"
397
+ ? body
398
+ : body.filter(part => part.type === "text").map(part => part.text).join("\n");
399
+ if (text === content) {
400
+ received = true;
401
+ acknowledge();
402
+ }
403
+ });
404
+ }
365
405
  if (session.isStreaming) {
366
406
  await session.sendUserMessage(content, { deliverAs: "steer" as const });
367
- return;
407
+ } else {
408
+ await runReadAgentSessionOperation(agent, () => session.sendUserMessage(content, undefined));
409
+ if (requireReceipt && !received) throw new Error(`Requester ${agent.name} finished without receiving the report.`);
368
410
  }
369
- await runReadAgentSessionOperation(agent, () => session.sendUserMessage(content, undefined));
370
411
  }
371
412
  );
372
413
  signalReadAgentWake(agent);
373
414
  pendingParentWakeSignals.get(agent)?.();
374
- await deliveryResult;
375
- markReadAgentActivity(agent, "received lead message", "thinking");
415
+ try {
416
+ await deliveryResult;
417
+ if (requireReceipt && !received) {
418
+ // Keep the receipt wait outside the raw tail so parent finalization can cancel an unconsumed steer.
419
+ await Promise.race([receipt, agent.messageDeliveryCancellation!.then(() => {
420
+ throw new ReadAgentDeliveryCancelledError(agent.name);
421
+ })]);
422
+ }
423
+ } catch (error) {
424
+ // Reporting closes admission while the idle SDK send still awaits the model turn.
425
+ if (!(received && error instanceof ReadAgentDeliveryCancelledError)) throw error;
426
+ } finally {
427
+ unsubscribe?.();
428
+ }
429
+ if (!agent.checkOperation && !agent.messageDeliveryClosed) markReadAgentActivity(agent, "received lead message", "thinking");
376
430
  return true;
377
431
  }
378
432
 
@@ -414,6 +468,8 @@ function reportPersistenceBlockReason(
414
468
  }
415
469
 
416
470
  async function recordReadAgentReportEvent(
471
+ state: RunningReadAgent,
472
+ isCurrent: () => boolean,
417
473
  teamName: string,
418
474
  member: Member,
419
475
  status: "completed" | "failed",
@@ -421,17 +477,29 @@ async function recordReadAgentReportEvent(
421
477
  summary: string,
422
478
  startedAt: number,
423
479
  tokensUsed: number,
480
+ result: ReportResult,
424
481
  costUsd?: number,
425
482
  color?: string,
426
483
  reportMetadata: Record<string, any> = {}
427
- ): Promise<{ persisted: true } | { persisted: false; error: unknown }> {
484
+ ): Promise<{ persisted: true; event: TeamReportEvent } | { persisted: false; error: unknown }> {
428
485
  const operation = operationMetadataFromMember(member);
429
486
  const modelSlot = canonicalPersistedModelSlot(member.modelSlot);
487
+ let checkpointOperation: RunningReadAgent["checkpointOperation"];
488
+ let settle = () => {};
430
489
  try {
431
- await reportEvents.appendTeamReportEvent(teamName, {
490
+ if (member.checkpointAssignment) {
491
+ if (state.stopRequested || !isCurrent()) throw new Error("Checkpoint publication cancelled: agent run is closing.");
492
+ if (state.checkpointOperation) throw new Error("Checkpoint publication is already active for this run.");
493
+ checkpointOperation = { controller: new AbortController(), settled: new Promise<void>(resolve => { settle = resolve; }) };
494
+ state.checkpointOperation = checkpointOperation;
495
+ }
496
+ const event = await reportEvents.appendTeamReportEvent(teamName, {
432
497
  agentName: member.name,
498
+ checkpoint: checkpointReference(teamName, member),
499
+ completionGroup: member.completionGroup,
433
500
  role: member.role || "read",
434
501
  status,
502
+ result,
435
503
  report,
436
504
  summary,
437
505
  startedAt,
@@ -453,9 +521,16 @@ async function recordReadAgentReportEvent(
453
521
  ...reportMetadata,
454
522
  },
455
523
  });
456
- return { persisted: true };
524
+ await saveReportCheckpoint(teamName, member, event, checkpointOperation?.controller.signal);
525
+ if (checkpointOperation && (checkpointOperation.controller.signal.aborted || state.stopRequested || !isCurrent())) throw new Error("Checkpoint publication cancelled: agent run is closing.");
526
+ if (event.checkpoint) result.checkpointId = event.checkpoint.id;
527
+ return { persisted: true, event };
457
528
  } catch (error) {
529
+ if (member.checkpointAssignment) state.finalizationBlockedReason ||= `Checkpoint publication failed: ${String(error)}`;
458
530
  return { persisted: false, error };
531
+ } finally {
532
+ if (checkpointOperation && state.checkpointOperation === checkpointOperation) state.checkpointOperation = undefined;
533
+ settle();
459
534
  }
460
535
  }
461
536
 
@@ -490,44 +565,43 @@ async function ensureReadHelperCompletionMessages(
490
565
  report: string,
491
566
  outcome: "completed" | "failed" = "completed",
492
567
  color = member.color,
493
- deliverMessageToActiveAgent?: RunReadAgentOptions["deliverMessageToActiveAgent"]
568
+ options: Pick<RunReadAgentOptions, "deliverMessageToActiveAgent" | "notifyLeadOfInboxReports"> = {}
494
569
  ): Promise<void> {
495
- if (!member.requestedBy) return;
570
+ const requestedBy = member.requestedBy;
571
+ if (!requestedBy) return;
496
572
 
497
- const expectedRequesterRunId = member.parentAgentName === member.requestedBy
498
- ? member.parentLifecycleRunId
499
- : undefined;
500
573
  let requesterReceivedReport = false;
574
+ // Direct session delivery has no inbox sender envelope. Keep identity outside the report body.
575
+ const attributedReport = `Read helper report ${JSON.stringify({ agentName: member.name, runId, runtimeStatus: outcome })}\n\n${report}`;
501
576
  try {
502
- const deliveredDirectly = expectedRequesterRunId
503
- ? await deliverMessageToActiveAgent?.(teamName, member.requestedBy, report, expectedRequesterRunId) === true
504
- : await deliverMessageToActiveAgent?.(teamName, member.requestedBy, report) === true;
505
- if (deliveredDirectly) {
506
- requesterReceivedReport = true;
507
- } else {
508
- requesterReceivedReport = await hasRecentMessageFrom(
509
- teamName,
510
- member.name,
511
- member.requestedBy,
512
- startedAt,
513
- message => message?.metadata?.helperReport === true && message?.metadata?.runId === runId
514
- );
515
- if (!requesterReceivedReport) {
516
- await messaging.sendPlainMessageIfRunning(
517
- teamName,
518
- member.name,
519
- member.requestedBy,
520
- report,
521
- outcome === "failed" ? `Read helper ${member.name} failed` : `Read helper ${member.name} report`,
522
- color,
523
- {
524
- metadata: { helperReport: true, helperCompletion: true, runId, outcome, requestedBy: member.requestedBy },
525
- ...(expectedRequesterRunId ? { expectedRecipientRunId: expectedRequesterRunId } : {}),
526
- }
527
- );
528
- requesterReceivedReport = true;
577
+ const requester = (await teams.readConfig(teamName)).members.find(item => item.name === member.requestedBy);
578
+ const expectedRequesterRunId = member.parentAgentName === member.requestedBy
579
+ ? member.parentLifecycleRunId : requester?.lifecycleRunId;
580
+ await messaging.sendPlainMessageOnceIfRunning(
581
+ teamName, member.name, requestedBy, report,
582
+ outcome === "failed" ? `Read helper ${member.name} failed` : `Read helper ${member.name} report`,
583
+ {
584
+ color,
585
+ operationId: `helper-report:${runId}`,
586
+ expectedRecipientRunId: expectedRequesterRunId,
587
+ metadata: { helperReport: true, helperCompletion: true, runId, outcome, requestedBy: member.requestedBy },
529
588
  }
530
- }
589
+ );
590
+ requesterReceivedReport = true;
591
+ // Durability, not the requester's complete idle model run, gates helper cleanup.
592
+ // Even an already persisted report must wake its exact requester automatically.
593
+ const wake = Promise.resolve().then(() => options.deliverMessageToActiveAgent?.(
594
+ teamName, requestedBy, attributedReport, expectedRequesterRunId, true
595
+ ));
596
+ void wake.then(delivered => {
597
+ if (!delivered) throw new Error("requester unavailable for automatic delivery");
598
+ }).catch(async error => {
599
+ await messaging.sendPlainMessage(teamName, member.name, "team-lead",
600
+ `Automatic report delivery to ${member.requestedBy} could not be confirmed: ${String(error)}. The full report is retained below; do not replay it automatically into a replacement run.\n\n${attributedReport}`,
601
+ `Read helper ${member.name} wake incomplete`, color,
602
+ { metadata: { helperWakeFailed: true, runId, requestedBy: member.requestedBy, expectedRecipientRunId: expectedRequesterRunId } });
603
+ await options.notifyLeadOfInboxReports?.(teamName);
604
+ }).catch(() => { console.warn(`Could not record helper wake outcome for ${member.name} (${runId}).`); });
531
605
  } catch {
532
606
  requesterReceivedReport = false;
533
607
  }
@@ -549,9 +623,7 @@ async function ensureReadHelperCompletionMessages(
549
623
  teamName,
550
624
  member.name,
551
625
  "team-lead",
552
- outcome === "failed"
553
- ? `Read helper ${member.name} failed for ${member.requestedBy}. ${delivery}`
554
- : `Read helper ${member.name} completed for ${member.requestedBy}. ${delivery}`,
626
+ `Read helper ${member.name} ${outcome} for ${requestedBy}. ${delivery}${requesterReceivedReport ? "" : `\n\n${attributedReport}`}`,
555
627
  outcome === "failed" ? `Read helper ${member.name} failed` : `Read helper ${member.name} done`,
556
628
  color,
557
629
  { metadata: { finalReport: true, helperCompletion: true, runId, outcome, requestedBy: member.requestedBy } }
@@ -597,6 +669,12 @@ export async function runReadAgentInProcess(
597
669
  }
598
670
  member.lifecycleRunId = lifecycleRunId;
599
671
  const extensionInstanceId = options.extensionInstanceId ?? generateExtensionInstanceId();
672
+ let costRun: CostRun | undefined;
673
+ let costSnapshot: AgentCostSnapshot | undefined;
674
+ let costOutcome: CostOutcome = "cancelled";
675
+ try {
676
+ costRun = options.beginCostRun?.(ctx.sessionManager.getSessionId(), readTeamName, lifecycleRunId);
677
+ } catch { /* Additive telemetry must not prevent an otherwise valid launch. */ }
600
678
  const state: RunningReadAgent = {
601
679
  runId: lifecycleRunId,
602
680
  name: member.name,
@@ -616,6 +694,7 @@ export async function runReadAgentInProcess(
616
694
  startupState: "pending",
617
695
  sessionCreation,
618
696
  teardownState: "active",
697
+ onCostSettled: () => costRun?.settle(costSnapshot, costOutcome),
619
698
  };
620
699
  let sessionCreationSettled = false;
621
700
  const settleSessionCreation = (session: AgentSession | undefined): void => {
@@ -645,8 +724,30 @@ export async function runReadAgentInProcess(
645
724
  let submittedFinalReport: SubmittedAgentReport | undefined;
646
725
  let finalReportSubmissionInProgress = false;
647
726
  let childSessionManager: any;
727
+ let childLifecycleProbeUnsubscribe = (): void => {};
648
728
  let privateSessionDirectory: string | undefined;
649
- let privateCompletedReportPersisted = false;
729
+ let completedReportPersisted = false;
730
+ let resolvedTaskResult: ReportResult | undefined;
731
+ let assignedChecks: CheckDefinition[] | undefined;
732
+ let repairPolicy: RepairPolicy | undefined;
733
+ const deliveredRepairRequests = new Set<string>();
734
+ let repairCancellation: Promise<void> | undefined;
735
+ const cancelAutomaticRepair = async (): Promise<void> => {
736
+ const result = resolvedTaskResult;
737
+ if (!repairPolicy || result?.repair?.state !== "requested") return;
738
+ repairCancellation ??= (async () => {
739
+ try {
740
+ await new VerificationController({ teamName: readTeamName, result, cwd: member.cwd, checks: assignedChecks, repair: repairPolicy }).cancel();
741
+ result.repair = { ...result.repair!, state: "cancelled", outcome: "blocked" };
742
+ } catch (error) {
743
+ state.finalizationBlockedReason = `Repair cancellation could not be persisted: ${error instanceof Error ? error.message : String(error)}`;
744
+ result.verification.error = state.finalizationBlockedReason;
745
+ closeReadAgentMessageDelivery(state);
746
+ throw error;
747
+ }
748
+ })();
749
+ await repairCancellation;
750
+ };
650
751
  const pendingChildController = options.pendingChildController;
651
752
  const pendingChildParent: ParentRunIdentity | undefined = isEligibleNestedReadParent(member)
652
753
  ? {
@@ -681,6 +782,79 @@ export async function runReadAgentInProcess(
681
782
  return deliveryClose;
682
783
  };
683
784
 
785
+ const verifyTaskResult = async (reported: ReportedTaskDetails, signal?: AbortSignal, submissionId?: string): Promise<VerificationDecision> => {
786
+ if (state.checkOperation) throw new Error("Check verification is already active for this run.");
787
+ if (assignedChecks?.length && (state.stopRequested || state.messageDeliveryClosed || state.finalizationBlockedReason || !options.isCurrentReadAgentRun(key, state))) {
788
+ throw new Error("Check verification cancelled: agent run is closing.");
789
+ }
790
+ const result = createReportResult(readTeamName, member.name, state.runId, reported);
791
+ if (resolvedTaskResult?.repair) Object.assign(result, { verification: resolvedTaskResult.verification, repair: resolvedTaskResult.repair });
792
+ resolvedTaskResult = result;
793
+ if (!assignedChecks?.length) return { result, checks: [] };
794
+ let decision: VerificationDecision = { result, checks: [] };
795
+ const verificationController = new VerificationController({
796
+ teamName: readTeamName, result, cwd: member.cwd, checks: assignedChecks, repair: repairPolicy,
797
+ });
798
+ result.verification = { state: "pending", checkIds: assignedCheckIds(result, assignedChecks) };
799
+ const controller = new AbortController();
800
+ const abort = () => controller.abort();
801
+ signal?.addEventListener("abort", abort, { once: true });
802
+ if (signal?.aborted) abort();
803
+ let settle!: () => void;
804
+ const operation = { controller, settled: new Promise<void>(resolve => { settle = resolve; }) };
805
+ state.checkOperation = operation;
806
+ markReadAgentActivity(state, "running assigned checks", "working", "assigned-check");
807
+ options.renderReadAgentStatus();
808
+ try {
809
+ try {
810
+ decision = await verificationController.verify(submissionId ?? "", {
811
+ loadOperations: options.loadCheckOperations ?? loadNativeCheckOperations,
812
+ signal: controller.signal,
813
+ });
814
+ Object.assign(result, decision.result);
815
+ decision.result = result;
816
+ } catch (error) {
817
+ state.finalizationBlockedReason = `Assigned check evidence could not be persisted: ${error instanceof Error ? error.message : String(error)}`;
818
+ result.verification.error = state.finalizationBlockedReason;
819
+ const repair = verificationController.pendingRepair(error);
820
+ if (repair) result.repair = repair;
821
+ throw error;
822
+ }
823
+ if (result.verification.state === "pending") {
824
+ state.finalizationBlockedReason = "Assigned checks have an unresolved execution claim; inspect the durable journal before finalizing this run.";
825
+ throw new Error(state.finalizationBlockedReason);
826
+ }
827
+ if (controller.signal.aborted || state.stopRequested || !options.isCurrentReadAgentRun(key, state)) {
828
+ await cancelAutomaticRepair();
829
+ throw new Error("Assigned check verification was cancelled.");
830
+ }
831
+ return decision;
832
+ } finally {
833
+ signal?.removeEventListener("abort", abort);
834
+ if (state.checkOperation === operation) state.checkOperation = undefined;
835
+ if (controller.signal.aborted && !state.messageDeliveryClosed && !state.stopRequested && options.isCurrentReadAgentRun(key, state)) {
836
+ state.acceptingMessages = true;
837
+ }
838
+ settle();
839
+ }
840
+ };
841
+
842
+ const deliverGroupedReport = async (event: TeamReportEvent, leadReport: string): Promise<void> => {
843
+ const planning = isPiPromptPlanningMember(member);
844
+ const suppressed = shouldSuppressLeadReportInjection(member);
845
+ const emitsEvent = planning || (!suppressed && !options.isTeammate
846
+ && (options.getTeamName() === readTeamName || readTeamName.startsWith("prompt-build-")));
847
+ const grouped = await deliverCompletionGroupReport(event, () => {
848
+ if (emitsEvent) options.emitAgentReport(readTeamName, member.name, state.startedAt, state.tokensUsed,
849
+ planning ? event.report : leadReport, event.status === "completed", true);
850
+ });
851
+ if (!grouped) throw new Error("Grouped report provenance is unavailable.");
852
+ if (!suppressed && !readTeamName.startsWith("prompt-build-")) {
853
+ await options.renderLeadInboxStatus?.().catch(() => {});
854
+ await options.notifyLeadOfInboxReports?.(readTeamName).catch(() => {});
855
+ }
856
+ };
857
+
684
858
  const deliverCompletion = async (
685
859
  resolution: ResolvedReadAgentReport,
686
860
  completionSummary: string,
@@ -690,6 +864,13 @@ export async function runReadAgentInProcess(
690
864
  const session = state.session;
691
865
  if (!session) throw new Error(`Agent ${member.name} completed without a nested session.`);
692
866
  const report = resolution.report!;
867
+ const result = resolvedTaskResult ?? createReportResult(readTeamName, member.name, state.runId, resolution);
868
+ resolvedTaskResult = result;
869
+ const checkpoint = checkpointReference(readTeamName, member);
870
+ if (checkpoint) result.checkpointId = checkpoint.id;
871
+ const leadReport = (assignedChecks?.length
872
+ ? `${report}\n\nHarness verification: ${result.verification.state}; lead acceptance: ${result.acceptance.state}${result.repair ? `; repair: ${result.repair.state}; effective task: ${effectiveTaskOutcome(result) ?? "unspecified"}` : ""}. Evidence: ${result.reportId}.`
873
+ : report) + (checkpoint ? `\n\nCheckpoint reference: ${checkpoint.id}.` : "");
693
874
  const completionStats = session.getSessionStats();
694
875
  // Private child transcripts are deleted after teardown; never publish pointers
695
876
  // that would outlive a successful run's recovery artifact.
@@ -709,6 +890,7 @@ export async function runReadAgentInProcess(
709
890
  name: member.name,
710
891
  role,
711
892
  status: "completed",
893
+ result,
712
894
  report,
713
895
  summary: completionSummary,
714
896
  completedAt: Date.now(),
@@ -738,6 +920,8 @@ export async function runReadAgentInProcess(
738
920
  ...reportMetadata,
739
921
  };
740
922
  const reportEventPersistence = await recordReadAgentReportEvent(
923
+ state,
924
+ () => options.isCurrentReadAgentRun(key, state),
741
925
  readTeamName,
742
926
  member,
743
927
  "completed",
@@ -745,6 +929,7 @@ export async function runReadAgentInProcess(
745
929
  completionSummary,
746
930
  state.startedAt,
747
931
  state.tokensUsed,
932
+ result,
748
933
  completionStats.cost,
749
934
  undefined,
750
935
  reportMetadata,
@@ -757,14 +942,17 @@ export async function runReadAgentInProcess(
757
942
  );
758
943
  throw new Error(state.finalizationBlockedReason);
759
944
  }
945
+ if (checkpoint && (state.stopRequested || !options.isCurrentReadAgentRun(key, state))) return;
946
+ completedReportPersisted = true;
760
947
  // The lifecycle finalizer consumes this flag only after session disposal and
761
948
  // successful lifecycle finalization, so every successful private run is removed.
762
949
  if (privateSessionDirectory) {
763
950
  state.cleanupPrivateSessionOnFinalize = true;
764
- privateCompletedReportPersisted = true;
765
951
  }
766
952
  const suppressLeadReportInjection = shouldSuppressLeadReportInjection(member);
767
- if (member.requestedBy) {
953
+ if (member.completionGroup && !member.requestedBy) {
954
+ await deliverGroupedReport(reportEventPersistence.event, leadReport);
955
+ } else if (member.requestedBy) {
768
956
  await ensureReadHelperCompletionMessages(
769
957
  readTeamName,
770
958
  member,
@@ -773,7 +961,7 @@ export async function runReadAgentInProcess(
773
961
  report,
774
962
  "completed",
775
963
  member.color,
776
- options.deliverMessageToActiveAgent
964
+ options
777
965
  );
778
966
  await options.renderLeadInboxStatus?.().catch(() => {});
779
967
  await options.notifyLeadOfInboxReports?.(readTeamName).catch(() => {});
@@ -784,13 +972,17 @@ export async function runReadAgentInProcess(
784
972
  // Workflow orchestrators consume full reports from TeamReportEvent storage.
785
973
  // Pi Prompt consumes its writer report through the private event without a lead turn.
786
974
  } else if (!options.isTeammate && (options.getTeamName() === readTeamName || readTeamName.startsWith("prompt-build-"))) {
787
- options.emitAgentReport(readTeamName, member.name, state.startedAt, state.tokensUsed, report, true);
975
+ options.emitAgentReport(readTeamName, member.name, state.startedAt, state.tokensUsed, leadReport, true);
788
976
  } else {
789
- await ensureLeadCompletionMessage(readTeamName, member, state.startedAt, report, completionSummary, member.color, completionMetadata);
977
+ await ensureLeadCompletionMessage(readTeamName, member, state.startedAt, leadReport, completionSummary, member.color, completionMetadata);
790
978
  }
791
979
  };
792
980
 
793
981
  try {
982
+ assignedChecks = normalizeCheckPolicy(member.assignedChecks);
983
+ repairPolicy = normalizeRepairPolicy(member.repairPolicy);
984
+ if (repairPolicy && !assignedChecks?.length) throw new Error("Repair policy requires explicitly assigned checks.");
985
+ if (repairPolicy) repairCancellationHandlers.set(state, cancelAutomaticRepair);
794
986
  if (pendingChildParent && !pendingChildController) {
795
987
  throw new Error(`Eligible nested read parent ${member.name} requires a pending child controller.`);
796
988
  }
@@ -824,6 +1016,7 @@ export async function runReadAgentInProcess(
824
1016
 
825
1017
  const {
826
1018
  createAgentSession,
1019
+ createEventBus,
827
1020
  DefaultResourceLoader,
828
1021
  getAgentDir,
829
1022
  SessionManager,
@@ -843,16 +1036,21 @@ export async function runReadAgentInProcess(
843
1036
  const childSettingsManager = createSettingsManager(member.cwd, agentDir, {
844
1037
  projectTrusted: resourcePlan.trust.projectTrusted,
845
1038
  });
846
- const nestedReadAgentTools = isEligibleNestedReadParent(member)
847
- ? options.createNestedReadAgentTools?.({
1039
+ const nestedReadBinding = isEligibleNestedReadParent(member)
1040
+ ? {
848
1041
  teamName: readTeamName,
849
1042
  parent: member,
850
1043
  parentRunId: state.runId,
851
1044
  outerCtx: ctx,
852
- }) ?? []
1045
+ }
1046
+ : undefined;
1047
+ const nestedReadAgentTools = nestedReadBinding
1048
+ ? options.createNestedReadAgentTools?.(nestedReadBinding) ?? []
853
1049
  : [];
854
1050
  const nestedReadDelegationEnabled = nestedReadAgentTools.length > 0;
1051
+ const childEventBus = createEventBus();
855
1052
  const loader = new DefaultResourceLoader({
1053
+ eventBus: childEventBus,
856
1054
  cwd: member.cwd,
857
1055
  agentDir,
858
1056
  settingsManager: childSettingsManager,
@@ -880,7 +1078,9 @@ export async function runReadAgentInProcess(
880
1078
  ]
881
1079
  : ["You cannot spawn or create other agents. If another agent is needed, use send_message to ask team-lead; only the lead decides and performs the spawn."]),
882
1080
  "NEVER sleep, busy-wait, or poll. Do not use bash sleep, while-true, or any wait/poll loop. The extension wakes you when messages arrive.",
883
- "When finished, use report_and_exit with the complete required deliverable in content and only a short label in summary, then stop. Never replace required output with a summary. Do not wait for the lead to kill you — report and exit cleanly.",
1081
+ repairPolicy
1082
+ ? `Use report_and_exit with the complete deliverable in content. The lead authorized at most ${repairPolicy.maxAttempts} additional repair attempts for assigned checks. If the tool requests repair, stay active, inspect its observed evidence and repair only within your role and assigned scope. Report blocked or failed if you cannot continue safely. Stop only after the report is accepted; never replace the deliverable with a summary.`
1083
+ : "When finished, use report_and_exit with the complete required deliverable in content and only a short label in summary, then stop. Never replace required output with a summary. Do not wait for the lead to kill you — report and exit cleanly.",
884
1084
  ],
885
1085
  });
886
1086
  await loader.reload();
@@ -903,19 +1103,29 @@ export async function runReadAgentInProcess(
903
1103
  pushReadAgentEvent(state, status);
904
1104
  options.renderReadAgentStatus();
905
1105
  },
906
- onReportAndExit: async report => {
1106
+ repairEnabled: !!repairPolicy,
1107
+ onReportAndExit: async (report, signal, submissionId) => {
907
1108
  const content = nonEmptyReportText(report.content);
908
1109
  if (!content) throw new Error("Final report content must not be empty.");
909
1110
  if (submittedFinalReport || finalReportSubmissionInProgress) return { accepted: false };
910
1111
  finalReportSubmissionInProgress = true;
911
1112
  try {
1113
+ if (member.checkpointAssignment) preflightReportCheckpoint(readTeamName, member, createReportResult(readTeamName, member.name, state.runId, report));
1114
+ const decision = await verifyTaskResult(report, signal, submissionId ? `tool:${submissionId}` : undefined);
1115
+ if (decision.request) {
1116
+ deliveredRepairRequests.add(decision.request.id);
1117
+ return { accepted: false, verification: decision.result.verification, repairRequest: decision.request };
1118
+ }
1119
+ const result = decision.result;
912
1120
  const deliveryClose = await closeRecipient();
913
1121
  submittedFinalReport = {
1122
+ ...normalizeReportedTaskDetails(report),
914
1123
  content,
915
1124
  summary: nonEmptyReportText(report.summary),
916
1125
  };
917
1126
  return {
918
1127
  accepted: true,
1128
+ ...(assignedChecks?.length ? { verification: result.verification } : {}),
919
1129
  cancelledDeliveries: deliveryClose.cancelledDeliveries,
920
1130
  deliveryOutcome: deliveryClose.cancelledDeliveries > 0 ? "cancelled" : "none",
921
1131
  };
@@ -951,6 +1161,14 @@ export async function runReadAgentInProcess(
951
1161
  childSessionManager = privateSessionDirectory
952
1162
  ? SessionManager.create(member.cwd, privateSessionDirectory)
953
1163
  : SessionManager.create(member.cwd);
1164
+ try { costSnapshot = { childSessionId: childSessionManager.getSessionId(), costUsd: null }; } catch { /* Unknown provenance. */ }
1165
+ if (nestedReadBinding && options.nestedChildSnapshot) {
1166
+ const childSessionId = childSessionManager.getSessionId();
1167
+ childLifecycleProbeUnsubscribe = childEventBus.on(CHILD_AGENT_LIFECYCLE_PROBE, (payload: any) => {
1168
+ if (payload?.sessionId !== childSessionId || typeof payload.respond !== "function") return;
1169
+ payload.respond({ sessionId: childSessionId, ...options.nestedChildSnapshot!(nestedReadBinding) });
1170
+ });
1171
+ }
954
1172
  const parentModelRuntime: unknown = Reflect.get(ctx.modelRegistry, "runtime");
955
1173
  const { session } = await createAgentSession({
956
1174
  cwd: member.cwd,
@@ -968,7 +1186,23 @@ export async function runReadAgentInProcess(
968
1186
  } as Parameters<typeof createAgentSession>[0] & { modelRuntime?: unknown });
969
1187
 
970
1188
  state.session = session;
971
- const sessionLifecycle = installReadAgentSessionLifecycle(session);
1189
+ // Current hosts capture this public hook when a prompt starts; legacy hosts
1190
+ // retain their existing loop behavior. Never abort from the reporting tool.
1191
+ if (session.agent && "shouldStopAfterTurn" in session.agent) {
1192
+ const agent = session.agent as typeof session.agent & {
1193
+ shouldStopAfterTurn?: (...args: unknown[]) => boolean | Promise<boolean>;
1194
+ };
1195
+ const previous = agent.shouldStopAfterTurn;
1196
+ agent.shouldStopAfterTurn = async (...args) => {
1197
+ const stopped = await previous?.(...args);
1198
+ return submittedFinalReport !== undefined || stopped === true;
1199
+ };
1200
+ }
1201
+ const sessionLifecycle = installReadAgentSessionLifecycle(session, () => {
1202
+ if (handoffRequested) return;
1203
+ const own = recordedSessionCost(childSessionManager.getEntries());
1204
+ costSnapshot = { childSessionId: childSessionManager.getSessionId(), costUsd: own.complete ? own.usd : null };
1205
+ });
972
1206
  try {
973
1207
  if (typeof session.bindExtensions === "function") {
974
1208
  await (session.bindExtensions as (bindings: { mode: "print" }) => Promise<void>)({ mode: "print" });
@@ -1001,12 +1235,23 @@ export async function runReadAgentInProcess(
1001
1235
  }
1002
1236
  await teams.updateMember(readTeamName, member.name, { herdrPaneId: id, tmuxPaneId: "" });
1003
1237
  });
1238
+ const rejectActiveCheck = (): void => {
1239
+ if (state.checkOperation) {
1240
+ throw new Error("Cannot move agent to Herdr while an assigned check is active. Try again after it settles.");
1241
+ }
1242
+ };
1004
1243
  state.moveToHerdr = () => moving ??= (async () => {
1005
1244
  if (!options.isCurrentReadAgentRun(key, state) || (!handoffRequested && !state.acceptingMessages)) {
1006
1245
  throw new Error("The agent is already finishing.");
1007
1246
  }
1008
1247
  if (handoffRequested && !released) throw new Error("The current operation is still stopping. Try h after it settles.");
1009
- if (paneId) { herdrCommand("pane", "close", paneId); await recordPane(undefined); paneId = undefined; }
1248
+ if (paneId) {
1249
+ rejectActiveCheck();
1250
+ herdrCommand("pane", "close", paneId);
1251
+ await recordPane(undefined);
1252
+ paneId = undefined;
1253
+ }
1254
+ rejectActiveCheck();
1010
1255
  const sessionFile = childSessionManager.getSessionFile();
1011
1256
  if (!sessionFile || !fs.existsSync(sessionFile)) throw new Error("The agent has no saved session yet.");
1012
1257
  if (!handoffRequested) {
@@ -1037,6 +1282,7 @@ export async function runReadAgentInProcess(
1037
1282
  try {
1038
1283
  if (!handoffRequested) {
1039
1284
  handoffRequested = true;
1285
+ try { costRun?.exclude(); } catch { /* Terminal work is outside this total. */ }
1040
1286
  state.stopRequested = true;
1041
1287
  const delivery = closeReadAgentMessageDelivery(state);
1042
1288
  const pending = session.clearQueue();
@@ -1214,6 +1460,65 @@ export async function runReadAgentInProcess(
1214
1460
  }
1215
1461
  }
1216
1462
  }
1463
+ while (assignedChecks?.length && !submittedFinalReport && !pendingChildParent
1464
+ && !state.stopRequested && options.isCurrentReadAgentRun(key, state)) {
1465
+ const wakeGeneration = readAgentWakeGeneration(state);
1466
+ // A delivery may have started before checks owned the operation gate.
1467
+ // Drain every admitted turn before capturing or verifying its final report.
1468
+ let deliveryTail: Promise<void> | undefined;
1469
+ do {
1470
+ deliveryTail = state.messageDeliveryTail;
1471
+ if (deliveryTail) await deliveryTail.catch(() => {});
1472
+ } while (deliveryTail !== state.messageDeliveryTail);
1473
+ if (state.stopRequested || !options.isCurrentReadAgentRun(key, state)) break;
1474
+ completionResolution = resolveCurrentReport();
1475
+ if (submittedFinalReport) break;
1476
+ if (state.completedOperationError) throw state.completedOperationError;
1477
+ if (state.completedOperationInterrupted) {
1478
+ if (!await waitForPostInterruptOperation(state.completedOperationGeneration!, wakeGeneration)) return;
1479
+ continue;
1480
+ }
1481
+ if (completionResolution.terminalFailure || !completionResolution.report) {
1482
+ throw unavailableReportError(completionResolution.terminalFailure ?? "No usable report is available for verification.");
1483
+ }
1484
+ const submissionId = `fallback:${state.completedOperationGeneration ?? 0}`;
1485
+ let decision: VerificationDecision | undefined;
1486
+ const verification = await runReadAgentSessionOperation(state, async () => {
1487
+ decision = await verifyTaskResult(completionResolution!, undefined, submissionId);
1488
+ });
1489
+ if (verification.interrupted) {
1490
+ if (!await waitForPostInterruptOperation(verification.generation, wakeGeneration)) return;
1491
+ } else {
1492
+ let deliveryTail: Promise<void> | undefined;
1493
+ do {
1494
+ deliveryTail = state.messageDeliveryTail;
1495
+ if (deliveryTail) await deliveryTail.catch(() => {});
1496
+ } while (deliveryTail !== state.messageDeliveryTail);
1497
+ state.acceptingMessages = false;
1498
+ const latestGeneration = state.completedOperationGeneration ?? verification.generation;
1499
+ if (latestGeneration === verification.generation) {
1500
+ const request = decision?.request;
1501
+ if (!request || deliveredRepairRequests.has(request.id)) break;
1502
+ deliveredRepairRequests.add(request.id);
1503
+ if (state.messageDeliveryClosed || state.stopRequested || !options.isCurrentReadAgentRun(key, state)) return;
1504
+ state.acceptingMessages = true;
1505
+ markReadAgentActivity(state, `repairing assigned checks (attempt ${request.attempt})`, "thinking");
1506
+ options.renderReadAgentStatus();
1507
+ const repairWake = readAgentWakeGeneration(state);
1508
+ const repairOperation = await runReadAgentSessionOperation(state, () => session.prompt(formatRepairRequest(request), { source: "extension" as any }));
1509
+ refreshReadAgentStats(state, session);
1510
+ if (repairOperation.interrupted && !await waitForPostInterruptOperation(repairOperation.generation, repairWake)) return;
1511
+ completionResolution = resolveCurrentReport();
1512
+ continue;
1513
+ }
1514
+ if (state.completedOperationError) throw state.completedOperationError;
1515
+ if (state.completedOperationInterrupted) {
1516
+ state.acceptingMessages = !state.messageDeliveryClosed && !state.stopRequested && options.isCurrentReadAgentRun(key, state);
1517
+ if (!await waitForPostInterruptOperation(latestGeneration, wakeGeneration)) return;
1518
+ }
1519
+ }
1520
+ completionResolution = resolveCurrentReport();
1521
+ }
1217
1522
  } finally {
1218
1523
  state.acceptingMessages = false;
1219
1524
  }
@@ -1240,13 +1545,16 @@ export async function runReadAgentInProcess(
1240
1545
  ?? `${role === "write" ? "Edit" : "Read"} agent ${member.name} completed`;
1241
1546
  const recoveryReference = readAgentRecoveryReference(readTeamName, member.name, state.runId, childSessionManager);
1242
1547
  await deliverCompletion(completionResolution, completionSummary, recoveryAttempted, recoveryReference);
1548
+ costOutcome = "completed";
1243
1549
  } catch (e) {
1550
+ await cancelAutomaticRepair().catch(() => {});
1551
+ if (!state.stopRequested) costOutcome = "failed";
1244
1552
  const lastError = runtime.createRuntimeError(e);
1245
1553
  state.lastError = lastError;
1246
1554
  options.renderReadAgentStatus();
1247
1555
  settleSessionCreation(state.session);
1248
1556
  await closeRecipient();
1249
- if (!state.stopRequested && options.isCurrentReadAgentRun(key, state) && !privateCompletedReportPersisted) {
1557
+ if (!state.stopRequested && options.isCurrentReadAgentRun(key, state) && !completedReportPersisted) {
1250
1558
  let failureReport = `${role === "write" ? "Edit" : "Read"} agent ${member.name} failed: ${e instanceof Error ? e.message : String(e)}`;
1251
1559
  const failureStats = state.session?.getSessionStats();
1252
1560
  const reportUnavailable = e instanceof ReadAgentReportUnavailableError ? e : undefined;
@@ -1264,7 +1572,13 @@ export async function runReadAgentInProcess(
1264
1572
  : {}),
1265
1573
  };
1266
1574
  const failureSummary = `${role === "write" ? "Edit" : "Read"} agent ${member.name} failed`;
1575
+ const result = resolvedTaskResult ?? createReportResult(readTeamName, member.name, state.runId, {});
1576
+ if (!resolvedTaskResult && assignedChecks?.length) {
1577
+ result.verification = { state: "pending", checkIds: assignedCheckIds(result, assignedChecks) };
1578
+ }
1267
1579
  const failureEventPersistence = await recordReadAgentReportEvent(
1580
+ state,
1581
+ () => options.isCurrentReadAgentRun(key, state),
1268
1582
  readTeamName,
1269
1583
  member,
1270
1584
  "failed",
@@ -1272,6 +1586,7 @@ export async function runReadAgentInProcess(
1272
1586
  failureSummary,
1273
1587
  state.startedAt,
1274
1588
  state.tokensUsed,
1589
+ result,
1275
1590
  failureStats?.cost,
1276
1591
  "red",
1277
1592
  failureReportMetadata,
@@ -1288,10 +1603,12 @@ export async function runReadAgentInProcess(
1288
1603
  failureReport = `${failureReport}\n\n${state.finalizationBlockedReason}`;
1289
1604
  }
1290
1605
  }
1606
+ if (member.checkpointAssignment && (state.stopRequested || !options.isCurrentReadAgentRun(key, state))) return;
1291
1607
  options.rememberCompletedAgentReport(readTeamName, {
1292
1608
  name: member.name,
1293
1609
  role,
1294
1610
  status: "failed",
1611
+ result,
1295
1612
  report: failureReport,
1296
1613
  summary: failureSummary,
1297
1614
  completedAt: Date.now(),
@@ -1321,7 +1638,9 @@ export async function runReadAgentInProcess(
1321
1638
  ...failureReportMetadata,
1322
1639
  };
1323
1640
  const suppressLeadReportInjection = shouldSuppressLeadReportInjection(member);
1324
- if (member.requestedBy) {
1641
+ if (member.completionGroup && !member.requestedBy && failureEventPersistence.persisted) {
1642
+ await deliverGroupedReport(failureEventPersistence.event, failureReport);
1643
+ } else if (member.requestedBy) {
1325
1644
  await ensureReadHelperCompletionMessages(
1326
1645
  readTeamName,
1327
1646
  member,
@@ -1330,7 +1649,7 @@ export async function runReadAgentInProcess(
1330
1649
  failureReport,
1331
1650
  "failed",
1332
1651
  "red",
1333
- options.deliverMessageToActiveAgent
1652
+ options
1334
1653
  );
1335
1654
  await options.renderLeadInboxStatus?.().catch(() => {});
1336
1655
  await options.notifyLeadOfInboxReports?.(readTeamName).catch(() => {});
@@ -1354,6 +1673,9 @@ export async function runReadAgentInProcess(
1354
1673
  }
1355
1674
  }
1356
1675
  } finally {
1676
+ childLifecycleProbeUnsubscribe();
1677
+ if (state.stopRequested) await cancelAutomaticRepair().catch(() => {});
1678
+ repairCancellationHandlers.delete(state);
1357
1679
  pendingParentWakeSignals.delete(state);
1358
1680
  disposeReadAgentWake(state);
1359
1681
  settleSessionCreation(state.session);