pi-subagents 0.37.2 → 0.38.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.
@@ -65,6 +65,13 @@ import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
65
65
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
66
66
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
67
67
  import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
68
+ import {
69
+ SUBAGENT_STARTUP_RETRY_DELAYS_MS,
70
+ formatSubagentStartupRetryExhaustedError,
71
+ formatSubagentStartupRetryNote,
72
+ isRetryableSubagentStartupFailure,
73
+ waitForSubagentStartupRetry,
74
+ } from "../shared/subagent-startup-retry.ts";
68
75
  import { markProcessTerminalCandidateLeaseRelease, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
69
76
  import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
70
77
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
@@ -395,6 +402,8 @@ interface RunPiStreamingResult {
395
402
  exitCode: number | null;
396
403
  messages: Message[];
397
404
  usage: Usage;
405
+ toolCount: number;
406
+ durationMs: number;
398
407
  model?: string;
399
408
  error?: string;
400
409
  protocolError?: ProtocolOutputLimit;
@@ -434,6 +443,7 @@ function runPiStreaming(
434
443
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
435
444
  ): Promise<RunPiStreamingResult> {
436
445
  return new Promise((resolve) => {
446
+ const startedAt = Date.now();
437
447
  const processInstanceId = randomUUID();
438
448
  onWriterProcess?.({ state: "spawning" });
439
449
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
@@ -471,6 +481,7 @@ function runPiStreaming(
471
481
  let turnBudgetMessage: string | undefined;
472
482
  let turnBudget: TurnBudgetState | undefined;
473
483
  let observedMutationAttempt = false;
484
+ let toolCount = 0;
474
485
  const childWatchdogConfig = decodeChildWatchdogConfig(env?.[CHILD_WATCHDOG_CONFIG_ENV]);
475
486
  let childWatchdogState: ChildWatchdogStateSnapshot | undefined;
476
487
  let applyChildLifecycle = (_action: ChildLifecycleAction): void => {};
@@ -557,6 +568,7 @@ function runPiStreaming(
557
568
  onChildEvent?.(event);
558
569
 
559
570
  if (event.type === "tool_execution_start" && event.toolName) {
571
+ toolCount += 1;
560
572
  observedMutationAttempt = observedMutationAttempt || isMutatingTool(event.toolName, event.args);
561
573
  const toolArgs = extractToolArgsPreview(event.args ?? {});
562
574
  writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
@@ -796,6 +808,8 @@ function runPiStreaming(
796
808
  exitCode: timedOut || stopped ? 1 : turnBudgetExceeded ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
797
809
  messages,
798
810
  usage,
811
+ toolCount,
812
+ durationMs: Date.now() - startedAt,
799
813
  model,
800
814
  error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
801
815
  protocolError,
@@ -833,7 +847,7 @@ function runPiStreaming(
833
847
  const stderr = stderrTail.text();
834
848
  const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
835
849
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
836
- resolve({ stderr, exitCode: 1, messages, usage, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, watchdog: childWatchdogState, processInstanceId });
850
+ resolve({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, watchdog: childWatchdogState, processInstanceId });
837
851
  });
838
852
  });
839
853
  }
@@ -1145,9 +1159,11 @@ async function runSingleStep(
1145
1159
  let toolBudgetBlocked = false;
1146
1160
  let actualLaunchContractDigest = step.launchContractDigest;
1147
1161
 
1148
- for (let index = 0; index < candidates.length; index++) {
1149
- if (ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
1150
- const candidate = candidates[index];
1162
+ let modelIndex = 0;
1163
+ let startupAttemptIndex = 0;
1164
+ modelAttemptsLoop: while (modelIndex < candidates.length) {
1165
+ if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1166
+ const candidate = candidates[modelIndex];
1151
1167
  ctx.onAttemptStart?.({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) });
1152
1168
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
1153
1169
  if (effectiveStructuredOutput) {
@@ -1259,7 +1275,7 @@ async function runSingleStep(
1259
1275
  writerProcesses.push({
1260
1276
  processInstanceId: run.processInstanceId,
1261
1277
  kind: "pi-writer",
1262
- attempt: index,
1278
+ attempt: writerAttemptCount - 1,
1263
1279
  closeObservedAt: run.processCloseObservedAt,
1264
1280
  exitCode: run.exitCode,
1265
1281
  signal: run.processSignal ?? null,
@@ -1358,7 +1374,7 @@ async function runSingleStep(
1358
1374
  usage: run.usage,
1359
1375
  };
1360
1376
  modelAttempts.push(attempt);
1361
- if (candidate) attemptedModels.push(candidate);
1377
+ if (candidate && startupAttemptIndex === 0) attemptedModels.push(candidate);
1362
1378
  completionGuardTriggeredFinal = completionGuardTriggered;
1363
1379
  finalOutputSnapshot = outputSnapshot;
1364
1380
  if (step.toolBudget) {
@@ -1368,11 +1384,55 @@ async function runSingleStep(
1368
1384
  toolBudget = toolBudgetState(step.toolBudget, toolMessages.length, blockedMessage ? (blockedMessage as { toolName?: string }).toolName : undefined);
1369
1385
  }
1370
1386
  finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect ? { effects: { fileMutation: fileMutationEffect } } : {}) } as RunPiStreamingResult & { structuredOutput?: unknown; agentContract?: import("../../shared/types.ts").AgentContract; effects?: import("../../shared/types.ts").EffectsProjection };
1371
- if (run.turnBudgetExceeded) break;
1372
- if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1373
- if (attempt.success || completionGuardTriggered) break;
1374
- if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
1375
- attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
1387
+ if (run.turnBudgetExceeded) break modelAttemptsLoop;
1388
+ if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break modelAttemptsLoop;
1389
+ if (attempt.success || completionGuardTriggered) break modelAttemptsLoop;
1390
+
1391
+ const startupFailure = isRetryableSubagentStartupFailure({
1392
+ exitCode: effectiveExitCode,
1393
+ error,
1394
+ finalOutput: run.finalOutput,
1395
+ messageCount: run.messages.length,
1396
+ toolCount: run.toolCount,
1397
+ usage: run.usage,
1398
+ durationMs: run.durationMs,
1399
+ protocolError: run.protocolError,
1400
+ processSignal: run.processSignal,
1401
+ observedMutationAttempt: run.observedMutationAttempt,
1402
+ interrupted: run.interrupted,
1403
+ timedOut: run.timedOut,
1404
+ stopped: run.stopped,
1405
+ turnBudgetExceeded: run.turnBudgetExceeded,
1406
+ });
1407
+ const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
1408
+ if (startupFailure && retryDelayMs !== undefined) {
1409
+ const retryNote = formatSubagentStartupRetryNote({
1410
+ model: attempt.model,
1411
+ attempt: startupAttemptIndex + 1,
1412
+ maxAttempts: SUBAGENT_STARTUP_RETRY_DELAYS_MS.length + 1,
1413
+ delayMs: retryDelayMs,
1414
+ });
1415
+ const shouldRetry = await waitForSubagentStartupRetry(retryDelayMs, [ctx.timeoutSignal, ctx.stopSignal]);
1416
+ if (!shouldRetry || ctx.skipAcceptance?.()) break modelAttemptsLoop;
1417
+ attempt.error = retryNote;
1418
+ attemptNotes.push(retryNote);
1419
+ startupAttemptIndex += 1;
1420
+ continue;
1421
+ }
1422
+ if (startupFailure) {
1423
+ const startupError = formatSubagentStartupRetryExhaustedError({
1424
+ model: attempt.model,
1425
+ attempts: startupAttemptIndex + 1,
1426
+ });
1427
+ attempt.error = startupError;
1428
+ finalResult.error = startupError;
1429
+ finalResult.finalOutput = startupError;
1430
+ break modelAttemptsLoop;
1431
+ }
1432
+ if (!isRetryableModelFailure(error) || modelIndex === candidates.length - 1) break modelAttemptsLoop;
1433
+ attemptNotes.push(formatModelAttemptNote(attempt, candidates[modelIndex + 1]));
1434
+ modelIndex += 1;
1435
+ startupAttemptIndex = 0;
1376
1436
  }
1377
1437
 
1378
1438
  const rawOutput = finalResult?.finalOutput ?? "";
@@ -451,13 +451,13 @@ export class ChainClarifyComponent implements Component {
451
451
  return;
452
452
  }
453
453
 
454
- if (matchesKey(data, "up")) {
454
+ if (matchesKey(data, "up") || matchesKey(data, "k")) {
455
455
  this.selectedStep = Math.max(0, this.selectedStep - 1);
456
456
  this.tui.requestRender();
457
457
  return;
458
458
  }
459
459
 
460
- if (matchesKey(data, "down")) {
460
+ if (matchesKey(data, "down") || matchesKey(data, "j")) {
461
461
  const maxStep = Math.max(0, this.agentConfigs.length - 1);
462
462
  this.selectedStep = Math.min(maxStep, this.selectedStep + 1);
463
463
  this.tui.requestRender();
@@ -1123,7 +1123,7 @@ export class ChainClarifyComponent implements Component {
1123
1123
  private getFooterText(): string {
1124
1124
  return this.mode === 'single'
1125
1125
  ? " [Enter] Run • [Esc] Cancel "
1126
- : " [Enter] Run • [Esc] Cancel • [↑↓] Navigate ";
1126
+ : " [Enter] Run • [Esc] Cancel • [↑↓/jk] Navigate ";
1127
1127
  }
1128
1128
 
1129
1129
  private appendNotice(lines: string[]): void {
@@ -45,6 +45,9 @@ import {
45
45
  hasEmptyTerminalAssistantResponse,
46
46
  extractToolArgsPreview,
47
47
  extractTextFromContent,
48
+ boundStreamedRecentTools,
49
+ boundStreamedRecentOutput,
50
+ boundStreamedToolCalls,
48
51
  } from "../../shared/utils.ts";
49
52
  import { buildSkillInjection, resolveSkillsWithFallback } from "../../agents/skills.ts";
50
53
  import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
@@ -63,6 +66,13 @@ import {
63
66
  formatModelAttemptNote,
64
67
  isRetryableModelFailure,
65
68
  } from "../shared/model-fallback.ts";
69
+ import {
70
+ SUBAGENT_STARTUP_RETRY_DELAYS_MS,
71
+ formatSubagentStartupRetryExhaustedError,
72
+ formatSubagentStartupRetryNote,
73
+ isRetryableSubagentStartupFailure,
74
+ waitForSubagentStartupRetry,
75
+ } from "../shared/subagent-startup-retry.ts";
66
76
  import {
67
77
  createMutatingFailureState,
68
78
  didMutatingToolFail,
@@ -161,8 +171,8 @@ function snapshotProgress(progress: AgentProgress): AgentProgress {
161
171
  return {
162
172
  ...progress,
163
173
  skills: progress.skills ? [...progress.skills] : undefined,
164
- recentTools: progress.recentTools.map((tool) => ({ ...tool })),
165
- recentOutput: [...progress.recentOutput],
174
+ recentTools: boundStreamedRecentTools(progress.recentTools),
175
+ recentOutput: boundStreamedRecentOutput(progress.recentOutput),
166
176
  };
167
177
  }
168
178
 
@@ -188,6 +198,20 @@ function snapshotResult(result: SingleResult, progress: AgentProgress): SingleRe
188
198
  };
189
199
  }
190
200
 
201
+ /**
202
+ * Streaming variant of snapshotResult for `onUpdate` progress events: it drops the
203
+ * unbounded `messages` transcript in favour of compact tool-call summaries so a
204
+ * single `tool_execution_update` line stays well under the child-stdout protocol
205
+ * cap (`MAX_CHILD_PENDING_LINE_BYTES`). Non-streaming consumers such as
206
+ * detached-exit recovery call snapshotResult directly and keep the full transcript.
207
+ */
208
+ function snapshotStreamResult(result: SingleResult, progress: AgentProgress): SingleResult {
209
+ const snapshot = snapshotResult(result, progress);
210
+ snapshot.messages = undefined;
211
+ snapshot.toolCalls = boundStreamedToolCalls(result);
212
+ return snapshot;
213
+ }
214
+
191
215
  async function runSingleAttempt(
192
216
  runtimeCwd: string,
193
217
  agent: AgentConfig,
@@ -337,6 +361,10 @@ async function runSingleAttempt(
337
361
  recentOutput: [...shared.attemptNotes],
338
362
  toolCount: 0,
339
363
  tokens: 0,
364
+ ...(modelArg ? { model: modelArg } : {}),
365
+ ...(resolvedThinking ? { thinking: resolvedThinking } : {}),
366
+ inputTokens: 0,
367
+ outputTokens: 0,
340
368
  durationMs: 0,
341
369
  lastActivityAt: startTime,
342
370
  };
@@ -683,7 +711,7 @@ async function runSingleAttempt(
683
711
  const emitUpdateSnapshot = (text: string) => {
684
712
  if (!options.onUpdate || processClosed) return;
685
713
  const progressSnapshot = snapshotProgress(progress);
686
- const resultSnapshot = snapshotResult(result, progressSnapshot);
714
+ const resultSnapshot = snapshotStreamResult(result, progressSnapshot);
687
715
  const controlEvents = drainPendingControlEvents();
688
716
  options.onUpdate({
689
717
  content: [{ type: "text", text }],
@@ -703,6 +731,7 @@ async function runSingleAttempt(
703
731
  emitUpdateSnapshot(output || "(running...)");
704
732
  };
705
733
 
734
+ const rawStdoutTail = createBoundedByteTail();
706
735
  const processLine = (line: string) => {
707
736
  if (!line.trim()) return;
708
737
  jsonlWriter.writeLine(line);
@@ -710,6 +739,7 @@ async function runSingleAttempt(
710
739
  try {
711
740
  evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; args?: unknown; willRetry?: unknown };
712
741
  } catch {
742
+ rawStdoutTail.push(`${line}\n`);
713
743
  shared.transcriptWriter?.writeStdoutLine(line);
714
744
  // Non-JSON stdout lines are expected; only structured events are parsed.
715
745
  return;
@@ -805,8 +835,13 @@ async function runSingleAttempt(
805
835
  result.usage.cacheWrite += u.cacheWrite || 0;
806
836
  result.usage.cost += u.cost?.total || 0;
807
837
  progress.tokens = result.usage.input + result.usage.output;
838
+ progress.inputTokens = result.usage.input;
839
+ progress.outputTokens = result.usage.output;
840
+ }
841
+ if (evt.message.model) {
842
+ progress.model = evt.message.model;
843
+ if (!result.model) result.model = evt.message.model;
808
844
  }
809
- if (!result.model && evt.message.model) result.model = evt.message.model;
810
845
  if (evt.message.errorMessage) assistantError = evt.message.errorMessage;
811
846
  const assistantText = extractTextFromContent(evt.message.content);
812
847
  appendRecentOutput(progress, assistantText.split("\n").slice(-10));
@@ -938,12 +973,17 @@ async function runSingleAttempt(
938
973
  stdoutReader.end();
939
974
  stderrReader.end();
940
975
  const stderr = stderrTail.text();
976
+ const rawStdout = rawStdoutTail.text();
941
977
  let closeError = result.error ?? toolDiagnosticError ?? assistantError;
942
978
  const forcedDrainAfterFinalSuccess = forcedTerminationSignal && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !closeError;
979
+ if (code !== 0 && rawStdout.trim() && !closeError && !forcedDrainAfterFinalSuccess) {
980
+ closeError = rawStdout.trim();
981
+ }
943
982
  if (code !== 0 && stderr.trim() && !closeError && !forcedDrainAfterFinalSuccess) {
944
983
  closeError = stderr.trim();
945
984
  }
946
985
  const finalCode = forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
986
+ if (signal) result.processSignal = signal;
947
987
  if (detached) {
948
988
  const recoveredProgress = snapshotProgress(progress);
949
989
  const recoveredResult = snapshotResult(result, recoveredProgress);
@@ -1208,7 +1248,7 @@ async function runSingleAttempt(
1208
1248
  if (options.onUpdate) {
1209
1249
  const finalText = result.finalOutput || result.error || "(no output)";
1210
1250
  const progressSnapshot = snapshotProgress(progress);
1211
- const resultSnapshot = snapshotResult(result, progressSnapshot);
1251
+ const resultSnapshot = snapshotStreamResult(result, progressSnapshot);
1212
1252
  options.onUpdate({
1213
1253
  content: [{ type: "text", text: finalText }],
1214
1254
  details: {
@@ -1351,6 +1391,7 @@ export async function runSync(
1351
1391
  agent: agentName,
1352
1392
  task,
1353
1393
  exitCode: target.exitCode,
1394
+ processSignal: target.processSignal,
1354
1395
  usage: target.usage,
1355
1396
  model: target.model,
1356
1397
  attemptedModels: target.attemptedModels,
@@ -1416,47 +1457,111 @@ export async function runSync(
1416
1457
 
1417
1458
  let lastResult: SingleResult | undefined;
1418
1459
  const modelsToTry = candidates.length > 0 ? candidates : [undefined];
1419
- for (let i = 0; i < modelsToTry.length; i++) {
1420
- const candidate = modelsToTry[i];
1421
- const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
1422
- const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, detachedAwareOptions, {
1423
- sessionEnabled,
1424
- systemPrompt,
1425
- resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
1426
- skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
1427
- jsonlPath,
1428
- artifactPaths: artifactPathsResult,
1429
- transcriptWriter,
1430
- attemptNotes,
1431
- modelCandidates: candidates.map((candidate) => applyThinkingSuffix(candidate, options.thinkingOverride ?? agent.thinking, options.thinkingOverride !== undefined)),
1432
- outputSnapshot,
1433
- originalTask: task,
1434
- });
1435
- lastResult = result;
1436
- if (result.model) attemptedModels.push(result.model);
1437
- else if (candidate) attemptedModels.push(candidate);
1438
- sumUsage(aggregateUsage, result.usage);
1439
- totalToolCount += result.progressSummary?.toolCount ?? 0;
1440
- totalDurationMs += result.progressSummary?.durationMs ?? 0;
1441
- const attemptSucceeded = result.exitCode === 0 && !result.error;
1442
- const attempt: ModelAttempt = {
1443
- model: result.model ?? candidate ?? agent.model ?? "default",
1444
- success: attemptSucceeded,
1445
- exitCode: result.exitCode,
1446
- error: result.error,
1447
- usage: { ...result.usage },
1448
- };
1449
- modelAttempts.push(attempt);
1450
- if (result.detached || result.timedOut || result.turnBudgetExceeded) {
1451
- break;
1452
- }
1453
- if (attemptSucceeded) {
1454
- break;
1455
- }
1456
- if (!isRetryableModelFailure(result.error) || i === modelsToTry.length - 1) {
1460
+ modelAttemptsLoop: for (let modelIndex = 0; modelIndex < modelsToTry.length; modelIndex++) {
1461
+ const candidate = modelsToTry[modelIndex];
1462
+ for (let startupAttemptIndex = 0; ; startupAttemptIndex++) {
1463
+ const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
1464
+ const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, detachedAwareOptions, {
1465
+ sessionEnabled,
1466
+ systemPrompt,
1467
+ resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
1468
+ skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
1469
+ jsonlPath,
1470
+ artifactPaths: artifactPathsResult,
1471
+ transcriptWriter,
1472
+ attemptNotes,
1473
+ modelCandidates: candidates
1474
+ .map((modelCandidate) => applyThinkingSuffix(modelCandidate, options.thinkingOverride ?? agent.thinking, options.thinkingOverride !== undefined))
1475
+ .filter((modelCandidate): modelCandidate is string => Boolean(modelCandidate)),
1476
+ outputSnapshot,
1477
+ originalTask: task,
1478
+ });
1479
+ lastResult = result;
1480
+ if (startupAttemptIndex === 0) {
1481
+ if (result.model) attemptedModels.push(result.model);
1482
+ else if (candidate) attemptedModels.push(candidate);
1483
+ }
1484
+ sumUsage(aggregateUsage, result.usage);
1485
+ totalToolCount += result.progressSummary?.toolCount ?? 0;
1486
+ totalDurationMs += result.progressSummary?.durationMs ?? 0;
1487
+ const attemptSucceeded = result.exitCode === 0 && !result.error;
1488
+ const attempt: ModelAttempt = {
1489
+ model: result.model ?? candidate ?? agent.model ?? "default",
1490
+ success: attemptSucceeded,
1491
+ exitCode: result.exitCode,
1492
+ error: result.error,
1493
+ usage: { ...result.usage },
1494
+ };
1495
+ modelAttempts.push(attempt);
1496
+ if (result.detached || result.timedOut || result.turnBudgetExceeded) break modelAttemptsLoop;
1497
+ if (attemptSucceeded) break modelAttemptsLoop;
1498
+
1499
+ const startupFailure = isRetryableSubagentStartupFailure({
1500
+ exitCode: result.exitCode,
1501
+ error: result.error,
1502
+ finalOutput: result.finalOutput,
1503
+ messageCount: result.messages?.length ?? 0,
1504
+ toolCount: result.progressSummary?.toolCount ?? 0,
1505
+ usage: result.usage,
1506
+ durationMs: result.progressSummary?.durationMs ?? Number.POSITIVE_INFINITY,
1507
+ protocolError: result.protocolError,
1508
+ processSignal: result.processSignal,
1509
+ detached: result.detached,
1510
+ interrupted: result.interrupted,
1511
+ timedOut: result.timedOut,
1512
+ stopped: result.stopped,
1513
+ turnBudgetExceeded: result.turnBudgetExceeded,
1514
+ });
1515
+ const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
1516
+ if (startupFailure && retryDelayMs !== undefined) {
1517
+ const retryNote = formatSubagentStartupRetryNote({
1518
+ model: attempt.model,
1519
+ attempt: startupAttemptIndex + 1,
1520
+ maxAttempts: SUBAGENT_STARTUP_RETRY_DELAYS_MS.length + 1,
1521
+ delayMs: retryDelayMs,
1522
+ });
1523
+ const shouldRetry = await waitForSubagentStartupRetry(retryDelayMs, [options.signal, options.interruptSignal]);
1524
+ if (!shouldRetry) {
1525
+ if (options.interruptSignal?.aborted) {
1526
+ result.exitCode = 0;
1527
+ result.interrupted = true;
1528
+ result.error = undefined;
1529
+ result.finalOutput = "Interrupted. Waiting for explicit next action.";
1530
+ if (result.progress) {
1531
+ result.progress.status = "running";
1532
+ result.progress.error = undefined;
1533
+ }
1534
+ } else {
1535
+ const cancellationError = "Subagent startup retry cancelled before relaunch.";
1536
+ result.error = cancellationError;
1537
+ result.finalOutput = cancellationError;
1538
+ attempt.error = cancellationError;
1539
+ if (result.progress) result.progress.error = cancellationError;
1540
+ }
1541
+ break modelAttemptsLoop;
1542
+ }
1543
+ attempt.error = retryNote;
1544
+ attemptNotes.push(retryNote);
1545
+ continue;
1546
+ }
1547
+ if (startupFailure) {
1548
+ const startupError = formatSubagentStartupRetryExhaustedError({
1549
+ model: attempt.model,
1550
+ attempts: startupAttemptIndex + 1,
1551
+ });
1552
+ result.error = startupError;
1553
+ result.finalOutput = startupError;
1554
+ if (result.progress) {
1555
+ result.progress.error = startupError;
1556
+ result.progress.status = "failed";
1557
+ }
1558
+ attempt.error = startupError;
1559
+ break modelAttemptsLoop;
1560
+ }
1561
+ if (!isRetryableModelFailure(result.error) || modelIndex === modelsToTry.length - 1) break modelAttemptsLoop;
1562
+ attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[modelIndex + 1]));
1457
1563
  break;
1458
1564
  }
1459
- attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
1460
1565
  }
1461
1566
 
1462
1567
  const result = withRunContext(lastResult ?? {
@@ -1476,13 +1581,6 @@ export async function runSync(
1476
1581
  tokens: aggregateUsage.input + aggregateUsage.output,
1477
1582
  durationMs: totalDurationMs,
1478
1583
  };
1479
- if (attemptNotes.length > 0 && result.progress) {
1480
- result.progress.recentOutput = [...attemptNotes, ...result.progress.recentOutput];
1481
- if (result.progress.recentOutput.length > 50) {
1482
- result.progress.recentOutput.splice(50);
1483
- }
1484
- }
1485
-
1486
1584
  if (transcriptWriter) result.transcriptPath = artifactPathsResult?.transcriptPath;
1487
1585
  if (transcriptWriter?.getError()) result.transcriptError = transcriptWriter.getError();
1488
1586
 
@@ -16,6 +16,10 @@ function copyProgress(target: ForegroundChildControl, progress: AgentProgress |
16
16
  target.currentPath = progress.currentPath;
17
17
  target.turnCount = progress.turnCount;
18
18
  target.tokens = progress.tokens;
19
+ target.inputTokens = progress.inputTokens;
20
+ target.outputTokens = progress.outputTokens;
21
+ target.model = progress.model;
22
+ target.thinking = progress.thinking;
19
23
  target.toolCount = progress.toolCount;
20
24
  }
21
25
 
@@ -30,6 +34,10 @@ function syncCurrentChild(control: ForegroundRunControl, child: ForegroundChildC
30
34
  control.currentPath = child.currentPath;
31
35
  control.turnCount = child.turnCount;
32
36
  control.tokens = child.tokens;
37
+ control.inputTokens = child.inputTokens;
38
+ control.outputTokens = child.outputTokens;
39
+ control.model = child.model;
40
+ control.thinking = child.thinking;
33
41
  control.toolCount = child.toolCount;
34
42
  control.interrupt = child.interrupt;
35
43
  control.updatedAt = child.updatedAt;
@@ -45,6 +53,10 @@ function clearCurrentChild(control: ForegroundRunControl): void {
45
53
  control.currentPath = undefined;
46
54
  control.turnCount = undefined;
47
55
  control.tokens = undefined;
56
+ control.inputTokens = undefined;
57
+ control.outputTokens = undefined;
58
+ control.model = undefined;
59
+ control.thinking = undefined;
48
60
  control.toolCount = undefined;
49
61
  control.interrupt = undefined;
50
62
  }
@@ -4095,6 +4095,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4095
4095
  ? undefined
4096
4096
  : {
4097
4097
  runId,
4098
+ ...(deps.state.currentSessionId ? { sessionId: deps.state.currentSessionId } : {}),
4098
4099
  mode: foregroundMode,
4099
4100
  startedAt: Date.now(),
4100
4101
  updatedAt: Date.now(),
@@ -32,7 +32,7 @@ const LEVEL_RANK: Record<Exclude<AcceptanceLevel, "auto">, number> = {
32
32
  };
33
33
 
34
34
  const VALID_LEVELS = new Set<AcceptanceLevel>(["auto", "none", "attested", "checked", "verified"]);
35
- const VALID_EVIDENCE = new Set<AcceptanceEvidenceKind>([
35
+ const VALID_EVIDENCE_KINDS: AcceptanceEvidenceKind[] = [
36
36
  "changed-files",
37
37
  "tests-added",
38
38
  "commands-run",
@@ -42,7 +42,10 @@ const VALID_EVIDENCE = new Set<AcceptanceEvidenceKind>([
42
42
  "diff-summary",
43
43
  "review-findings",
44
44
  "manual-notes",
45
- ]);
45
+ ];
46
+ const VALID_EVIDENCE = new Set<AcceptanceEvidenceKind>(VALID_EVIDENCE_KINDS);
47
+ const ACCEPTANCE_EVIDENCE_HELP = `Supported evidence kinds: ${VALID_EVIDENCE_KINDS.join(", ")}. Example: { level: "checked", evidence: ["commands-run", "changed-files"] }.`;
48
+ const ACCEPTANCE_OBJECT_EXAMPLE = "Example: { level: \"checked\", evidence: [\"commands-run\", \"changed-files\"] }.";
46
49
  const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "criteria", "evidence", "verify", "review", "stopRules", "reason"]);
47
50
  const ACCEPTANCE_GATE_KEYS = new Set(["id", "must", "evidence", "severity"]);
48
51
  const ACCEPTANCE_VERIFY_KEYS = new Set(["id", "command", "timeoutMs", "cwd", "env", "allowFailure"]);
@@ -153,6 +156,11 @@ function explicitAcceptanceCanDisable(explicit: AcceptanceConfig): boolean {
153
156
  return explicit.level === "none" && typeof explicit.reason === "string" && explicit.reason.trim().length > 0;
154
157
  }
155
158
 
159
+ function unsupportedEvidenceKindMessage(pathLabel: string, item: unknown): string {
160
+ const value = typeof item === "string" ? ` "${item}"` : "";
161
+ return `${pathLabel}${value} is not a supported evidence kind. ${ACCEPTANCE_EVIDENCE_HELP}`;
162
+ }
163
+
156
164
  export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"): string[] {
157
165
  const errors: string[] = [];
158
166
  if (input === undefined) return errors;
@@ -164,7 +172,7 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
164
172
  return errors;
165
173
  }
166
174
  if (!input || typeof input !== "object" || Array.isArray(input)) {
167
- errors.push(`${pathLabel} must be a string level, false, or an object.`);
175
+ errors.push(`${pathLabel} must be a string level, false, or an object. ${ACCEPTANCE_OBJECT_EXAMPLE}`);
168
176
  return errors;
169
177
  }
170
178
  const value = input as Record<string, unknown>;
@@ -202,11 +210,11 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
202
210
  criterionIds.add(normalizedId);
203
211
  }
204
212
  if (typeof gate.must !== "string" || !gate.must.trim()) errors.push(`${criterionPath}.must is required.`);
205
- if (gate.evidence !== undefined && !Array.isArray(gate.evidence)) errors.push(`${criterionPath}.evidence must be an array.`);
213
+ if (gate.evidence !== undefined && !Array.isArray(gate.evidence)) errors.push(`${criterionPath}.evidence must be an array. ${ACCEPTANCE_EVIDENCE_HELP}`);
206
214
  if (Array.isArray(gate.evidence)) {
207
215
  for (const [evidenceIndex, item] of gate.evidence.entries()) {
208
216
  if (typeof item !== "string" || !VALID_EVIDENCE.has(item as AcceptanceEvidenceKind)) {
209
- errors.push(`${criterionPath}.evidence[${evidenceIndex}] is not a supported evidence kind.`);
217
+ errors.push(unsupportedEvidenceKindMessage(`${criterionPath}.evidence[${evidenceIndex}]`, item));
210
218
  }
211
219
  }
212
220
  }
@@ -218,11 +226,11 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
218
226
  if (Array.isArray(value.evidence)) {
219
227
  for (const [index, item] of value.evidence.entries()) {
220
228
  if (typeof item !== "string" || !VALID_EVIDENCE.has(item as AcceptanceEvidenceKind)) {
221
- errors.push(`${pathLabel}.evidence[${index}] is not a supported evidence kind.`);
229
+ errors.push(unsupportedEvidenceKindMessage(`${pathLabel}.evidence[${index}]`, item));
222
230
  }
223
231
  }
224
232
  } else if (value.evidence !== undefined) {
225
- errors.push(`${pathLabel}.evidence must be an array.`);
233
+ errors.push(`${pathLabel}.evidence must be an array. ${ACCEPTANCE_EVIDENCE_HELP}`);
226
234
  }
227
235
  if (value.verify !== undefined && !Array.isArray(value.verify)) errors.push(`${pathLabel}.verify must be an array.`);
228
236
  if (Array.isArray(value.verify)) {
@@ -38,6 +38,8 @@ export const SUBAGENT_PARENT_SESSION_ENV = "PI_SUBAGENT_PARENT_SESSION";
38
38
  export const SUBAGENT_STEER_INBOX_ENV = "PI_SUBAGENT_STEER_INBOX";
39
39
  export const SUBAGENT_STEER_CAPABILITY_ENV = "PI_SUBAGENT_STEER_CAPABILITY";
40
40
  export const SUBAGENT_STEER_ACK_DIR_ENV = "PI_SUBAGENT_STEER_ACK_DIR";
41
+ export const PI_INTERCOM_STABLE_ID_ENV = "PI_INTERCOM_STABLE_ID";
42
+ export const PI_INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID";
41
43
 
42
44
  export interface BuildPiArgsInput {
43
45
  parentSessionId?: string;
@@ -331,6 +333,8 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
331
333
  : "";
332
334
  env.PI_SUBAGENT_INHERIT_PROJECT_CONTEXT = input.inheritProjectContext ? "1" : "0";
333
335
  env.PI_SUBAGENT_INHERIT_SKILLS = input.inheritSkills ? "1" : "0";
336
+ env[PI_INTERCOM_STABLE_ID_ENV] = input.intercomSessionName || undefined;
337
+ env[PI_INTERCOM_SESSION_ID_ENV] = undefined;
334
338
  if (input.intercomSessionName) {
335
339
  env.PI_SUBAGENT_INTERCOM_SESSION_NAME = input.intercomSessionName;
336
340
  }