pi-subagents 0.67.0 → 0.68.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 (120) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +1 -1
  3. package/docs/agents.md +37 -12
  4. package/docs/configuration.md +61 -19
  5. package/docs/extension-api.md +5 -1
  6. package/docs/missions.md +2 -2
  7. package/docs/models.md +11 -79
  8. package/docs/observability.md +18 -8
  9. package/docs/standalone-background.md +13 -3
  10. package/docs/tool-reference.md +15 -12
  11. package/docs/watchdog.md +10 -12
  12. package/docs/workflows.md +11 -1
  13. package/index.ts +5 -2
  14. package/package.json +4 -2
  15. package/runner-peer-loader.mjs +24 -0
  16. package/runner-peer-preload.mjs +25 -11
  17. package/skills/pi-subagents/SKILL.md +18 -21
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  19. package/skills/pi-subagents/references/execution-controls.md +4 -4
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  21. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  22. package/skills/pi-subagents/references/prompting-and-roles.md +16 -12
  23. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  24. package/src/agents/agent-management.ts +57 -58
  25. package/src/agents/agent-serializer.ts +4 -3
  26. package/src/agents/agents.ts +184 -71
  27. package/src/agents/chain-serializer.ts +5 -0
  28. package/src/agents/runtime-agent-registry.ts +7 -6
  29. package/src/api/preflight.ts +20 -16
  30. package/src/api/required-child-extensions.ts +6 -0
  31. package/src/extension/config.ts +10 -37
  32. package/src/extension/fanout-child.ts +3 -0
  33. package/src/extension/herdr-pi-bridge.ts +160 -0
  34. package/src/extension/index.ts +42 -31
  35. package/src/extension/public-execution.ts +3 -3
  36. package/src/extension/schemas.ts +16 -5
  37. package/src/extension/tool-description.ts +8 -7
  38. package/src/intercom/native-supervisor-channel.ts +22 -18
  39. package/src/policy/authority.ts +4 -0
  40. package/src/profiles/profiles.ts +12 -6
  41. package/src/runs/background/active-run-index.ts +17 -1
  42. package/src/runs/background/async-execution.ts +309 -126
  43. package/src/runs/background/async-job-tracker.ts +8 -6
  44. package/src/runs/background/async-resume.ts +13 -4
  45. package/src/runs/background/async-status.ts +15 -4
  46. package/src/runs/background/auto-drain.ts +20 -10
  47. package/src/runs/background/binary-bootstrap.ts +5 -0
  48. package/src/runs/background/chain-append.ts +1 -1
  49. package/src/runs/background/chain-root-attachment.ts +14 -33
  50. package/src/runs/background/notify.ts +74 -6
  51. package/src/runs/background/result-files.ts +8 -4
  52. package/src/runs/background/result-watcher.ts +19 -2
  53. package/src/runs/background/run-child-session.ts +20 -29
  54. package/src/runs/background/runner-aliases.ts +4 -33
  55. package/src/runs/background/runner-child-launch.ts +4 -1
  56. package/src/runs/background/runner-child-sessions.ts +2 -2
  57. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  58. package/src/runs/background/scheduled-runs.ts +11 -5
  59. package/src/runs/background/stale-run-reconciler.ts +35 -11
  60. package/src/runs/background/subagent-runner.ts +396 -275
  61. package/src/runs/background/subagent-wait.ts +128 -23
  62. package/src/runs/background/wait-completions.ts +75 -27
  63. package/src/runs/background/wait-subscriptions.ts +9 -3
  64. package/src/runs/background/wait-tool.ts +4 -2
  65. package/src/runs/foreground/async-stop-action.ts +93 -3
  66. package/src/runs/foreground/execution.ts +91 -218
  67. package/src/runs/foreground/foreground-history.ts +2 -1
  68. package/src/runs/foreground/subagent-executor.ts +266 -80
  69. package/src/runs/shared/acceptance.ts +34 -10
  70. package/src/runs/shared/async-status-projection.ts +123 -33
  71. package/src/runs/shared/child-launch-plan.ts +15 -3
  72. package/src/runs/shared/child-launch.ts +19 -6
  73. package/src/runs/shared/child-runtime-config.ts +5 -0
  74. package/src/runs/shared/child-session.ts +94 -50
  75. package/src/runs/shared/child-tool-plan.ts +28 -16
  76. package/src/runs/shared/dynamic-fanout.ts +2 -2
  77. package/src/runs/shared/external-cli-contract.ts +11 -1
  78. package/src/runs/shared/external-cli-preflight.ts +6 -2
  79. package/src/runs/shared/herdr-connection.ts +134 -0
  80. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  81. package/src/runs/shared/herdr-machine.ts +279 -0
  82. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  83. package/src/runs/shared/herdr-placed-run.ts +263 -0
  84. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  85. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
  86. package/src/runs/shared/model-scope.ts +1 -1
  87. package/src/runs/shared/nested-events.ts +11 -2
  88. package/src/runs/shared/parallel-utils.ts +7 -2
  89. package/src/runs/shared/pi-spawn.ts +1 -1
  90. package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
  91. package/src/runs/shared/worktree-setup-command.ts +27 -4
  92. package/src/runs/shared/worktree.ts +3 -3
  93. package/src/shared/child-cache-retention.ts +43 -0
  94. package/src/shared/launch-contract.ts +6 -9
  95. package/src/shared/pruned-fork.ts +1 -1
  96. package/src/shared/required-child-extensions.ts +81 -0
  97. package/src/shared/settings.ts +5 -2
  98. package/src/shared/shortcuts.ts +0 -4
  99. package/src/shared/types.ts +70 -29
  100. package/src/slash/slash-commands.ts +0 -6
  101. package/src/slash/subagents-admin.ts +13 -9
  102. package/src/tui/render.ts +20 -10
  103. package/src/watchdog/child-status.ts +28 -36
  104. package/src/watchdog/lsp-diagnostics.ts +1 -1
  105. package/src/watchdog/model-selection.ts +1 -1
  106. package/src/watchdog/register-child.ts +10 -3
  107. package/src/watchdog/register-main.ts +20 -20
  108. package/src/watchdog/render.ts +1 -1
  109. package/src/watchdog/review.ts +14 -30
  110. package/src/watchdog/rules.ts +1 -1
  111. package/src/watchdog/runtime.ts +23 -12
  112. package/src/watchdog/settings.ts +3 -6
  113. package/src/watchdog/types.ts +3 -5
  114. package/src/watchdog/warning-format.ts +1 -1
  115. package/src/workflows/scripted-workflow.ts +42 -3
  116. package/src/workflows/workflow-receipt.ts +21 -3
  117. package/src/workflows/workflow-resources.ts +13 -2
  118. package/src/runs/shared/model-exclusions.ts +0 -374
  119. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  120. package/src/runs/shared/readonly-session-evidence.ts +0 -307
@@ -1,7 +1,12 @@
1
+ import * as fs from "node:fs";
1
2
  import * as path from "node:path";
2
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
3
- import type { Details, SubagentState } from "../../shared/types.ts";
4
+ import { writeAtomicJson } from "../../shared/atomic-json.ts";
5
+ import { DIRS, type AsyncStatus, type Details, type SubagentState } from "../../shared/types.ts";
6
+ import { updateActiveRunIndex } from "../background/active-run-index.ts";
4
7
  import { deliverStopRequest } from "../background/control-channel.ts";
8
+ import { readProcessTerminal } from "../background/process-terminal.ts";
9
+ import { resultFilePath, resultPayloadPathForSessionRun, writeAsyncResultFile } from "../background/result-files.ts";
5
10
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
6
11
  import { isStoppableAsyncStatusStep, resolveAsyncStatusChild, type ResolvedAsyncStatusChild } from "../shared/child-identity.ts";
7
12
 
@@ -21,6 +26,80 @@ function getAsyncStopTarget(
21
26
  return direct ? { asyncId: direct.asyncId, asyncDir: direct.asyncDir } : undefined;
22
27
  }
23
28
 
29
+ const STOP_MESSAGE = "Subagent stopped by user.";
30
+
31
+ function sealPausedRun(asyncDir: string, status: AsyncStatus): string | undefined {
32
+ const runnerProcessInstanceId = status.processTerminal?.runnerProcessInstanceId;
33
+ if (!runnerProcessInstanceId) return "runner process identity is missing";
34
+ const proof = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId });
35
+ if (proof?.state !== "observed" || proof.runId !== status.runId || proof.runnerProcessInstanceId !== runnerProcessInstanceId) {
36
+ return `process-terminal proof is ${proof?.state === "unknown" ? `unknown (${proof.reason})` : proof?.state ?? "missing"}`;
37
+ }
38
+ if (!status.sessionId) return "session identity is missing";
39
+ const existingResultPath = resultPayloadPathForSessionRun(DIRS.results, status.sessionId, status.runId);
40
+ if (!existingResultPath) return "paused result is missing";
41
+ let existingResult: Record<string, unknown>;
42
+ try {
43
+ const parsed: unknown = JSON.parse(fs.readFileSync(existingResultPath, "utf-8"));
44
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("result is not an object");
45
+ existingResult = parsed as Record<string, unknown>;
46
+ } catch (error) {
47
+ return `paused result is unavailable: ${error instanceof Error ? error.message : String(error)}`;
48
+ }
49
+ const resultRunId = typeof existingResult.runId === "string" ? existingResult.runId : existingResult.id;
50
+ if (resultRunId !== status.runId || existingResult.sessionId !== status.sessionId) {
51
+ return "paused result identity does not match the run";
52
+ }
53
+
54
+ const now = Date.now();
55
+ const steps = (status.steps ?? []).map((step) => {
56
+ if (step.status !== "pending" && step.status !== "running" && step.status !== "paused") return step;
57
+ const { activityState: _activityState, ...rest } = step;
58
+ return {
59
+ ...rest,
60
+ status: "stopped" as const,
61
+ error: STOP_MESSAGE,
62
+ exitCode: 1,
63
+ stopped: true,
64
+ endedAt: now,
65
+ durationMs: step.startedAt ? now - step.startedAt : 0,
66
+ lastActivityAt: now,
67
+ };
68
+ });
69
+ const results = Array.isArray(existingResult.results)
70
+ ? existingResult.results.map((entry, index) => {
71
+ const stepState = status.steps?.[index]?.status;
72
+ if ((stepState !== "pending" && stepState !== "running" && stepState !== "paused") || !entry || typeof entry !== "object" || Array.isArray(entry)) return entry;
73
+ const { interrupted: _interrupted, ...result } = entry as Record<string, unknown>;
74
+ return { ...result, output: STOP_MESSAGE, error: STOP_MESSAGE, success: false, exitCode: 1, stopped: true };
75
+ })
76
+ : existingResult.results;
77
+ const stoppedResult = {
78
+ ...existingResult,
79
+ success: false,
80
+ state: "stopped",
81
+ summary: STOP_MESSAGE,
82
+ error: STOP_MESSAGE,
83
+ stopped: true,
84
+ exitCode: 1,
85
+ timestamp: now,
86
+ ...(Array.isArray(existingResult.results) ? { results } : {}),
87
+ };
88
+ const stoppedStatus: AsyncStatus = {
89
+ ...status,
90
+ state: "stopped",
91
+ stopped: true,
92
+ error: STOP_MESSAGE,
93
+ endedAt: now,
94
+ lastUpdate: now,
95
+ steps,
96
+ };
97
+ delete stoppedStatus.activityState;
98
+ writeAsyncResultFile(resultFilePath(DIRS.results, status.runId), stoppedResult);
99
+ writeAtomicJson(path.join(asyncDir, "status.json"), stoppedStatus);
100
+ updateActiveRunIndex(asyncDir, "stopped", status.toolCallId, { retryCapacityErrors: true, terminalIndexBeforeRelease: true });
101
+ }
102
+
24
103
  export function stopAsyncRun(
25
104
  state: SubagentState,
26
105
  runId: string | undefined,
@@ -38,7 +117,8 @@ export function stopAsyncRun(
38
117
  details: { mode: "management", results: [] },
39
118
  };
40
119
  }
41
- if (!status || (status.state !== "running" && status.state !== "queued")) {
120
+ const pausedWholeRun = status?.state === "paused" && childId === undefined;
121
+ if (!status || (status.state !== "running" && status.state !== "queued" && !pausedWholeRun)) {
42
122
  return {
43
123
  content: [{ type: "text", text: `No running or queued async run was found for '${runId ?? "current"}'.` }],
44
124
  isError: true,
@@ -66,13 +146,23 @@ export function stopAsyncRun(
66
146
  }
67
147
  try {
68
148
  deliverStopRequest({ asyncDir: target.asyncDir, pid: typeof status.pid === "number" ? status.pid : undefined, kill, source: "stop-action", targetIndex: child?.index, childId: child?.id ?? childId });
149
+ if (pausedWholeRun) {
150
+ const failure = sealPausedRun(target.asyncDir, status);
151
+ if (failure) {
152
+ return {
153
+ content: [{ type: "text", text: `Stop request persisted for paused async run ${target.asyncId}, but terminal proof is not ready (${failure}). Retry stop after runner shutdown is observed.` }],
154
+ isError: true,
155
+ details: { mode: "management", results: [] },
156
+ };
157
+ }
158
+ }
69
159
  const tracked = state.asyncJobs.get(target.asyncId);
70
160
  if (tracked) {
71
161
  tracked.activityState = undefined;
72
162
  tracked.updatedAt = Date.now();
73
163
  }
74
164
  return {
75
- content: [{ type: "text", text: child ? `Stop requested for child ${child.id} in async run ${target.asyncId}.` : `Stop requested for async run ${target.asyncId}.` }],
165
+ content: [{ type: "text", text: pausedWholeRun ? `Stopped paused async run ${target.asyncId}.` : child ? `Stop requested for child ${child.id} in async run ${target.asyncId}.` : `Stop requested for async run ${target.asyncId}.` }],
76
166
  details: { mode: "management", results: [] },
77
167
  };
78
168
  } catch (error) {
@@ -20,7 +20,6 @@ import {
20
20
  type AgentProgress,
21
21
  type ArtifactPaths,
22
22
  type ControlEvent,
23
- type ModelAttempt,
24
23
  type RunSyncOptions,
25
24
  type SingleResult,
26
25
  type Usage,
@@ -56,10 +55,6 @@ import { resolveSkillsWithFallback } from "../../agents/skills.ts";
56
55
  import { effectiveToolTimeoutMs, formatToolTimeoutMessage, resolveToolTimeoutMs, toolTimeoutCallKey, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
57
56
  import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMutationToolCapability, validateImplementationToolContract } from "../shared/completion-guard.ts";
58
57
  import { planCompletionEvidence } from "../shared/completion-evidence.ts";
59
- import { planAbortRecovery } from "../shared/abort-recovery.ts";
60
- import { planReadonlyModelContinuation, type LogicalRecoveryState } from "../shared/readonly-model-continuation.ts";
61
- import { getReadonlySessionEvidence, requestReadonlySessionEvidence, type SettledReadonlyEvidence } from "../shared/readonly-session-evidence.ts";
62
- import { getReadonlyChildModels } from "../shared/child-session.ts";
63
58
  import { arbitrateCompletionGuardRescue } from "../shared/llm-intent-arbiter.ts";
64
59
  import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
65
60
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
@@ -73,16 +68,15 @@ import { assertThinkingWithinCeiling, intersectThinkingCeilings } from "../../sh
73
68
  import { MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, MISSING_STRUCTURED_OUTPUT_CALL_ERROR } from "../shared/structured-output.ts";
74
69
  import { formatMidToolExitError, isOrdinaryToolForMidToolExit } from "../shared/process-signal.ts";
75
70
  import { formatChildToolDiagnostic } from "../shared/tool-availability.ts";
71
+ import { formatChildModelResolutionDiagnostic, isChildModelResolutionFailure } from "../shared/model-resolution-diagnostic.ts";
72
+ import { planAbortRecovery } from "../shared/abort-recovery.ts";
76
73
  import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
77
74
  import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, hasSingleOutputChangedSinceSnapshot, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
78
75
  import {
79
- buildModelCandidates,
80
76
  formatSubagentModelVerificationError,
81
- formatModelAttemptNote,
82
77
  isContextOverflow,
83
- isRetryableModelFailureAttempt,
84
- recordRetryableModelFailure,
85
- } from "../shared/model-fallback.ts";
78
+ resolveModelSelection,
79
+ } from "../shared/model-resolution.ts";
86
80
  import {
87
81
  createMutatingFailureState,
88
82
  didMutatingToolFail,
@@ -160,8 +154,7 @@ function persistSingleResultMetadata(input: {
160
154
  processSignal: target.processSignal,
161
155
  usage: target.usage,
162
156
  model: target.model,
163
- attemptedModels: target.attemptedModels,
164
- modelAttempts: target.modelAttempts,
157
+ requestedModel: target.requestedModel,
165
158
  durationMs: target.progressSummary?.durationMs,
166
159
  toolCount: target.progressSummary?.toolCount,
167
160
  error: target.error,
@@ -261,13 +254,6 @@ function snapshotResult(result: SingleResult, progress: AgentProgress): SingleRe
261
254
  messages: result.outputMode === "file-only" && result.savedOutputPath ? undefined : result.messages ? [...result.messages] : undefined,
262
255
  usage: { ...result.usage },
263
256
  skills: result.skills ? [...result.skills] : undefined,
264
- attemptedModels: result.attemptedModels ? [...result.attemptedModels] : undefined,
265
- modelAttempts: result.modelAttempts
266
- ? result.modelAttempts.map((attempt) => ({
267
- ...attempt,
268
- usage: attempt.usage ? { ...attempt.usage } : undefined,
269
- }))
270
- : undefined,
271
257
  controlEvents: result.controlEvents ? result.controlEvents.map((event) => ({ ...event })) : undefined,
272
258
  progress,
273
259
  progressSummary: result.progressSummary ? { ...result.progressSummary } : undefined,
@@ -340,11 +326,10 @@ function structuredDelegationProgressChanged(
340
326
  return false;
341
327
  }
342
328
 
343
- const AFTER_COMPACTION_SETTLEMENT = Symbol("afterCompactionSettlement");
344
- type AbortRecoverySingleResult = SingleResult & { [AFTER_COMPACTION_SETTLEMENT]?: true };
345
- const settledReadonlySource = new WeakMap<SingleResult, ChildSession>();
346
329
 
347
330
  const STOPPED_BEFORE_COMPLETION_ERROR = "Subagent stopped before completion.";
331
+ const AFTER_COMPACTION_SETTLEMENT = Symbol("afterCompactionSettlement");
332
+ type AbortRecoverySingleResult = SingleResult & { [AFTER_COMPACTION_SETTLEMENT]?: true };
348
333
 
349
334
 
350
335
  async function runSingleAttempt(
@@ -358,7 +343,6 @@ async function runSingleAttempt(
358
343
  systemPrompt: string;
359
344
  acceptancePrompt: string;
360
345
  resolvedSkillNames?: string[];
361
- modelCandidates?: string[];
362
346
  skillsWarning?: string;
363
347
  jsonlPath?: string;
364
348
  artifactPaths?: ArtifactPaths;
@@ -369,9 +353,6 @@ async function runSingleAttempt(
369
353
  orcaProgressTab?: OrcaProgressTab;
370
354
  launchWarnings: { emitted: boolean };
371
355
  verifyModel: boolean;
372
- readonlyExpected?: SettledReadonlyEvidence;
373
- readonlyModel?: string;
374
- readonlyHandoffAllowed?: () => boolean;
375
356
  },
376
357
  ): Promise<SingleResult> {
377
358
  const effectiveThinking = options.thinkingOverride ?? agent.thinking;
@@ -398,7 +379,11 @@ async function runSingleAttempt(
398
379
  : undefined;
399
380
  let onWatchdogStatus: ((event: ChildWatchdogStatusEvent) => void) | undefined;
400
381
  const launch = buildInProcessChildLaunch({
382
+ machine: options.machine,
383
+ remoteSkillNames: options.skills ?? agent.skills,
384
+ remoteReads: options.remoteReads,
401
385
  extensionBindings: options.extensionBindings,
386
+ requiredExtensions: options.requiredExtensions,
402
387
  sessionEnabled: shared.sessionEnabled,
403
388
  sessionDir: options.sessionDir,
404
389
  sessionFile: options.sessionFile,
@@ -429,7 +414,6 @@ async function runSingleAttempt(
429
414
  forkCacheKey: options.context === "fork" ? deriveForkPromptCacheKey(options.parentSessionId) : undefined,
430
415
  structuredOutput: options.structuredOutput,
431
416
  fast: options.fast ?? agent.fast,
432
- modelCandidates: shared.modelCandidates,
433
417
  toolBudget: options.toolBudget,
434
418
  permissionRules,
435
419
  permissionAuditPath,
@@ -446,6 +430,7 @@ async function runSingleAttempt(
446
430
  inherited: options.childRuntime,
447
431
  host: "parent",
448
432
  });
433
+ if (!options.machine && options.parentProviderRegistry) launch.session.parentProviderRegistry = options.parentProviderRegistry;
449
434
  const { toolPlan, capabilityAudit, warnings, launchResolvedExtensions, capture } = launch;
450
435
  if (!shared.launchWarnings.emitted && warnings.length > 0) {
451
436
  for (const warning of warnings) console.warn(`[pi-subagents] ${warning}`);
@@ -476,8 +461,6 @@ async function runSingleAttempt(
476
461
  error: contractError,
477
462
  usage: emptyUsage(),
478
463
  model: modelArg,
479
- modelAttempts: [],
480
- attemptedModels: [],
481
464
  progressSummary: { status: "failed", toolCount: 0, tokens: 0, durationMs: 0 },
482
465
  ...(toolPlan.capabilityCeiling ? { capabilityCeiling: toolPlan.capabilityCeiling } : {}),
483
466
  ...(toolPlan.capabilityAudit ? { capabilityAudit: toolPlan.capabilityAudit } : {}),
@@ -487,7 +470,7 @@ async function runSingleAttempt(
487
470
  const { launchContractDigest } = resolveLaunchBinding({
488
471
  agent,
489
472
  task: shared.originalTask ?? task,
490
- modelCandidates: shared.modelCandidates ?? [],
473
+ model: modelArg,
491
474
  ...(fast !== undefined ? { fast } : {}),
492
475
  ...(resolvedThinking ? { thinking: resolvedThinking } : {}),
493
476
  systemPrompt: effectiveSystemPrompt,
@@ -568,12 +551,13 @@ async function runSingleAttempt(
568
551
  };
569
552
  return result;
570
553
  }
571
- const mutationSnapshot = snapshotTrackedMutations(options.cwd ?? runtimeCwd);
554
+ const mutationSnapshot = options.machine ? { source: "tracked-files" as const, trackedOnly: true as const, cwd: options.cwd ?? runtimeCwd, dirtyFiles: [], fingerprints: {}, unavailable: "Local Git evidence is not authoritative for a pane-native remote run." } : snapshotTrackedMutations(options.cwd ?? runtimeCwd);
572
555
  let observedMutationAttempt = false;
573
556
  let structuredOutputToolInvoked = false;
574
557
  let structuredOutputMessageStartIndex: number | undefined;
575
558
  let toolAvailabilityError: string | undefined;
576
559
  let abortedBySignal = options.signal?.aborted === true;
560
+ let afterCompactionSettlement = false;
577
561
 
578
562
  if (options.workflowChildPermitLaunch) {
579
563
  const permitError = consumeWorkflowChildPermit(options.workflowChildPermitLaunch.permit, {
@@ -594,7 +578,6 @@ async function runSingleAttempt(
594
578
  }
595
579
  }
596
580
  const childSessions = options.childSessionFactory ?? childSessionFactory();
597
- let afterCompactionSettlement = false;
598
581
  const exitCode = await new Promise<number>((resolve) => {
599
582
  const jsonlWriter = createJsonlWriter(shared.jsonlPath, { pause() {}, resume() {} });
600
583
  let session: ChildSession | undefined;
@@ -801,7 +784,6 @@ async function runSingleAttempt(
801
784
  });
802
785
  // Report the run only after the child's extensions have shut down.
803
786
  void Promise.resolve().then(() => session?.dispose()).catch(() => undefined).then(() => {
804
- if (session && getReadonlySessionEvidence(session)) settledReadonlySource.set(result, session);
805
787
  resolve(code);
806
788
  });
807
789
  };
@@ -1308,9 +1290,23 @@ async function runSingleAttempt(
1308
1290
  const toolDiagnosticError = diagnostic ? formatChildToolDiagnostic(diagnostic, { host: "parent" }) : undefined;
1309
1291
  toolAvailabilityError = toolDiagnosticError;
1310
1292
  result.runtimeAcknowledgedExtensions = capture.runtimeAcknowledgedExtensions();
1293
+ if (session?.machineEvidence) result.nativeMachine = { provider: "herdr", machineId: session.machineEvidence.machineId, ...(session.machineEvidence.initial ? { initialGit: session.machineEvidence.initial } : {}), ...(session.machineEvidence.final ? { finalGit: session.machineEvidence.final } : {}) };
1311
1294
  let closeError = result.error ?? toolDiagnosticError ?? assistantError;
1312
- if (!closeError && promptError !== undefined) {
1313
- closeError = promptError instanceof Error ? promptError.message : String(promptError);
1295
+ const promptErrorMessage = promptError === undefined ? undefined : promptError instanceof Error ? promptError.message : String(promptError);
1296
+ if (!closeError && promptErrorMessage !== undefined) {
1297
+ closeError = promptErrorMessage;
1298
+ }
1299
+ // A foreground child never loads the parent's ambient extensions, so a
1300
+ // provider one registers resolves as "not found" before the child starts.
1301
+ // Annotate only a creation/prompt failure that produced no turn; keep the
1302
+ // core error and add the host rule and both remedies after it.
1303
+ if (promptErrorMessage !== undefined
1304
+ && closeError === promptErrorMessage
1305
+ && isChildModelResolutionFailure(promptErrorMessage)
1306
+ && (result.messages?.length ?? 0) === 0
1307
+ && result.usage.turns === 0
1308
+ && !launch.session.ambientExtensions) {
1309
+ closeError = `${promptErrorMessage}\n\n${formatChildModelResolutionDiagnostic({ agent: agent.name, model: launch.session.model, host: "parent", capabilityCeiling: launch.toolPlan.capabilityCeiling })}`;
1314
1310
  }
1315
1311
  const forcedDrainAfterFinalSuccess = (forced || forcedTermination) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !closeError;
1316
1312
  const forcedDrainAfterEmptyTerminal = forcedDrainAfterFinalSuccess && hasEmptyTerminalAssistantResponse(result.messages ?? []);
@@ -1383,8 +1379,6 @@ async function runSingleAttempt(
1383
1379
  void (async () => {
1384
1380
  try {
1385
1381
  const input = createReportedChildSessionInput(launch, shared.transcriptWriter);
1386
- requestReadonlySessionEvidence(input, shared.readonlyExpected);
1387
- if (shared.readonlyHandoffAllowed && !shared.readonlyHandoffAllowed()) throw new Error("Read-only continuation handoff vetoed.");
1388
1382
  const created = await childSessions.create(input);
1389
1383
  if (lifecycleFinished) {
1390
1384
  void created.dispose();
@@ -1407,10 +1401,6 @@ async function runSingleAttempt(
1407
1401
  abortChild();
1408
1402
  }
1409
1403
  options.onChildSession?.({ steer: (text) => created.steer(text), followUp: (text) => created.followUp(text) });
1410
- const actualReadonlyModel = shared.readonlyExpected && getReadonlyChildModels(created)?.current;
1411
- if (shared.readonlyExpected && (!actualReadonlyModel || actualReadonlyModel.fullId !== shared.readonlyModel
1412
- || actualReadonlyModel.api !== shared.readonlyExpected.api || created.modelId !== shared.readonlyModel || abortedBySignal || interruptedByControl || result.timedOut
1413
- || !shared.readonlyHandoffAllowed?.())) throw new Error("Read-only continuation handoff vetoed.");
1414
1404
  await created.prompt(`Task: ${task}`);
1415
1405
  settle(undefined);
1416
1406
  } catch (error) {
@@ -1509,7 +1499,8 @@ async function runSingleAttempt(
1509
1499
  tokens: progress.tokens,
1510
1500
  durationMs: progress.durationMs,
1511
1501
  };
1512
- const mutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, options.cwd ?? runtimeCwd);
1502
+ const remoteGitChanged = result.nativeMachine?.initialGit && result.nativeMachine.finalGit ? result.nativeMachine.initialGit.head !== result.nativeMachine.finalGit.head || result.nativeMachine.initialGit.dirty !== result.nativeMachine.finalGit.dirty : undefined;
1503
+ const mutationEvidence = result.nativeMachine ? { source: "tracked-files" as const, trackedOnly: true as const, changedFiles: [], attemptedMutation: remoteGitChanged === true, ...(remoteGitChanged === undefined ? { unavailable: "Remote Git before/after evidence was incomplete." } : {}) } : collectTrackedMutationEvidence(mutationSnapshot, options.cwd ?? runtimeCwd);
1513
1504
 
1514
1505
  const acceptanceOutput = getFinalOutput(result.messages ?? []);
1515
1506
  let fullOutput = stripAcceptanceReport(acceptanceOutput);
@@ -1804,9 +1795,8 @@ async function runSyncCompletionInner(
1804
1795
  }
1805
1796
  const systemPrompt = buildEffectiveSystemPrompt({ agent, resolvedSkills, cwd: skillCwd, ...(options.outputPath ? { outputPath: options.outputPath } : {}) });
1806
1797
 
1807
- const candidates = buildModelCandidates(
1798
+ const { model: selectedModel, requestedModel } = resolveModelSelection(
1808
1799
  options.modelOverride ?? agent.model,
1809
- agent.fallbackModels,
1810
1800
  options.availableModels,
1811
1801
  agent.modelProvider ?? options.preferredModelProvider,
1812
1802
  {
@@ -1815,23 +1805,9 @@ async function runSyncCompletionInner(
1815
1805
  origin: options.modelOrigin ?? (options.modelOverrideFromParent ? "inherited" : "configured"),
1816
1806
  },
1817
1807
  );
1818
- if (options.workflowChildPermitLaunch && candidates.length > 1) {
1819
- const error = "Workflow child permit does not support model fallback.";
1820
- return redactResultPrompt(withRunContext({
1821
- index: options.index ?? 0,
1822
- agent: agent.name,
1823
- task,
1824
- exitCode: 1,
1825
- messages: [],
1826
- usage: emptyUsage(),
1827
- error,
1828
- }, options.context));
1829
- }
1830
1808
  try {
1831
- for (const candidate of candidates) {
1832
- const model = applyThinkingSuffix(candidate, options.thinkingOverride ?? agent.thinking, options.thinkingOverride !== undefined);
1833
- assertThinkingWithinCeiling({ model, configThinking: options.thinkingOverride ?? agent.thinking, ceiling: options.thinkingCeiling, agent: agent.name, runId: options.runId });
1834
- }
1809
+ const model = applyThinkingSuffix(selectedModel, options.thinkingOverride ?? agent.thinking, options.thinkingOverride !== undefined);
1810
+ assertThinkingWithinCeiling({ model, configThinking: options.thinkingOverride ?? agent.thinking, ceiling: options.thinkingCeiling, agent: agent.name, runId: options.runId });
1835
1811
  } catch (error) {
1836
1812
  return redactResultPrompt(withRunContext({
1837
1813
  index: options.index ?? 0,
@@ -1843,8 +1819,6 @@ async function runSyncCompletionInner(
1843
1819
  error: error instanceof Error ? error.message : String(error),
1844
1820
  }, options.context));
1845
1821
  }
1846
- const attemptedModels: string[] = [];
1847
- const modelAttempts: ModelAttempt[] = [];
1848
1822
  const aggregateUsage = emptyUsage();
1849
1823
  const attemptNotes: string[] = [];
1850
1824
  const launchWarnings = { emitted: false };
@@ -1895,10 +1869,11 @@ async function runSyncCompletionInner(
1895
1869
  });
1896
1870
  };
1897
1871
 
1898
- let intercomDetached = false;
1899
1872
  let detachedReason: string | undefined;
1873
+ const logicalDeadline = options.deadlineAt ?? (options.timeoutMs === undefined ? undefined : Date.now() + options.timeoutMs);
1900
1874
  const attemptOptions: RunSyncOptions = {
1901
1875
  ...options,
1876
+ deadlineAt: logicalDeadline,
1902
1877
  onDetachReceipt: (receipt) => {
1903
1878
  receipt.acceptance = buildPendingAcceptanceLedger(effectiveAcceptance);
1904
1879
  try {
@@ -1909,165 +1884,65 @@ async function runSyncCompletionInner(
1909
1884
  const accepted = options.onDetachReceipt?.(receipt) === true;
1910
1885
  if (accepted) {
1911
1886
  detachedReason = receipt.detachedReason;
1912
- if (receipt.detachedReason === "intercom coordination") intercomDetached = true;
1913
1887
  }
1914
1888
  return accepted;
1915
1889
  },
1916
1890
  };
1891
+ const candidate = selectedModel;
1892
+ const verifyModel = Boolean(candidate) && !options.modelOverrideFromParent;
1917
1893
  let lastResult: SingleResult | undefined;
1918
- const modelsToTry = candidates.length > 0 ? candidates : [undefined];
1919
- let recoveryState: LogicalRecoveryState = "unused";
1920
- let readonlyExpected: SettledReadonlyEvidence | undefined;
1921
- let readonlyModel: string | undefined;
1922
- let readonlySource: ChildSession | undefined;
1923
- // Ordinary startup retries retain their per-attempt timeout. Only retained
1924
- // continuation uses the original logical deadline, never a renewed allowance.
1925
- const continuationDeadline = options.deadlineAt ?? (options.timeoutMs === undefined ? undefined : Date.now() + options.timeoutMs);
1926
- const readonlyHandoffAllowed = () => !options.signal?.aborted && !options.interruptSignal?.aborted
1927
- && !intercomDetached && !detachedReason && !options.workflowChildPermitLaunch
1928
- && options.usageBudget === undefined && options.toolBudget === undefined
1929
- && (continuationDeadline === undefined || Date.now() < continuationDeadline)
1930
- && (!readonlySource || getReadonlySessionEvidence(readonlySource) === readonlyExpected)
1931
- && !readonlySource?.detached && !readonlySource?.shutDown;
1932
- let nextAttemptTask = task;
1933
- modelAttemptsLoop: for (let modelIndex = 0; modelIndex < modelsToTry.length; modelIndex++) {
1934
- const candidate = modelsToTry[modelIndex];
1935
- // The inner loop re-runs the same candidate at most once, for abort recovery.
1936
- for (;;) {
1937
- const recoveringAbort = recoveryState === "abort-recovery";
1938
- const attemptTask = nextAttemptTask;
1939
- const verifyModel = Boolean(candidate) && !(options.modelOverrideFromParent && modelIndex === 0);
1940
- const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
1941
- if (recoveryState === "readonly-continuation") attemptOptions.deadlineAt = continuationDeadline;
1942
- const result = await runSingleAttempt(runtimeCwd, agent, attemptTask, candidate, attemptOptions, {
1943
- sessionEnabled,
1944
- systemPrompt,
1945
- acceptancePrompt,
1946
- resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
1947
- skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
1948
- jsonlPath,
1949
- artifactPaths: artifactPathsResult,
1950
- transcriptWriter,
1951
- attemptNotes,
1952
- modelCandidates: candidates
1953
- .map((modelCandidate) => applyThinkingSuffix(modelCandidate, options.thinkingOverride ?? agent.thinking, options.thinkingOverride !== undefined))
1954
- .filter((modelCandidate): modelCandidate is string => Boolean(modelCandidate)),
1955
- outputSnapshot,
1956
- originalTask: task,
1957
- orcaProgressTab,
1958
- launchWarnings,
1959
- verifyModel,
1960
- readonlyExpected,
1961
- readonlyModel,
1962
- readonlyHandoffAllowed: readonlyExpected ? readonlyHandoffAllowed : undefined,
1963
- });
1964
- lastResult = result;
1965
- if (!recoveringAbort) {
1966
- if (result.model) attemptedModels.push(result.model);
1967
- else if (candidate) attemptedModels.push(candidate);
1968
- }
1969
- sumUsage(aggregateUsage, result.usage);
1970
- totalToolCount += result.progressSummary?.toolCount ?? 0;
1971
- totalDurationMs += result.progressSummary?.durationMs ?? 0;
1972
- const attemptSucceeded = result.exitCode === 0 && !result.error;
1973
- const attempt: ModelAttempt = {
1974
- model: result.model ?? candidate ?? agent.model ?? "default",
1975
- success: attemptSucceeded,
1976
- exitCode: result.exitCode,
1977
- error: result.error,
1978
- usage: { ...result.usage },
1979
- };
1980
- modelAttempts.push(attempt);
1981
- // A consumed retained continuation is terminal even on a startup error or abort.
1982
- if (recoveryState === "readonly-continuation") break modelAttemptsLoop;
1983
- const source = settledReadonlySource.get(result);
1984
- const evidence = source && getReadonlySessionEvidence(source);
1985
- const models = source && getReadonlyChildModels(source);
1986
- // Deny non-text retained inputs, including unknown blocks. Count bytes once,
1987
- // not restored usage (the latter belongs to earlier attempts/runs).
1988
- const textOnly = evidence && (JSON.parse(evidence.contextJson) as Array<{ role: string; content: unknown }>).every((message) =>
1989
- typeof message.content === "string" || Array.isArray(message.content) && message.content.every((block) =>
1990
- block?.type === "text" || message.role === "assistant" && block?.type === "toolCall"));
1991
- const retainedBytes = evidence && models ? Buffer.byteLength(evidence.contextJson) + models.requestBytes : Infinity;
1992
- const resolvedCandidates = modelsToTry.map((reference, index) => index === modelIndex ? models?.current : reference ? models?.resolve(reference) : undefined);
1993
- const continuation = planReadonlyModelContinuation({
1994
- source, recoveryState, currentIndex: modelIndex,
1995
- candidates: resolvedCandidates.map((resolved, index) => {
1996
- // A conservative byte ceiling includes serialized history, actual system
1997
- // prompt/tools, framing/continuation headroom and full output allowance.
1998
- const required = resolved?.maxTokens ? retainedBytes + 4096 + resolved.maxTokens : Infinity;
1999
- return {
2000
- resolved: resolved?.api ? { provider: resolved.provider, model: resolved.id, api: resolved.api } : undefined,
2001
- tried: index <= modelIndex,
2002
- compatibility: textOnly && resolved?.input?.includes("text") && (resolved.contextWindow ?? 0) >= required ? "compatible" as const : "unknown" as const,
2003
- };
2004
- }),
2005
- lifecycleAllowsContinuation: !attemptSucceeded && readonlyHandoffAllowed() && !result.stopped && !result.detached && !result.interrupted && !result.timedOut,
2006
- effectsAllowContinuation: !result.structuredOutputFailed && !result.toolBudgetBlocked && !result.progress?.currentTool
2007
- && !result.outputSaveError && (!result.effects?.fileMutation || result.effects.fileMutation.status === "not-applicable"),
2008
- budget: options.toolBudget ? "tool-budget-configured" : options.usageBudget ? "unknown" : "unconfigured",
2009
- knownContextOverflow: Boolean(result.contextOverflow || isContextOverflow(result.error)),
2010
- });
2011
- if (continuation.kind === "continue") {
2012
- recoveryState = continuation.recoveryState; // consume BEFORE any sibling creation
2013
- readonlyExpected = continuation.expected;
2014
- readonlySource = source;
2015
- readonlyModel = resolvedCandidates[continuation.candidateIndex]?.fullId;
2016
- nextAttemptTask = continuation.prompt;
2017
- attemptNotes.push(`[readonly-continuation] ${attempt.model} failed with HTTP 429 after read-only progress; continuing retained session once with ${readonlyModel}.`);
2018
- modelIndex = continuation.candidateIndex - 1;
2019
- continue modelAttemptsLoop;
2020
- }
2021
- if (!attemptSucceeded) {
2022
- const afterCompactionSettlement = (result as AbortRecoverySingleResult)[AFTER_COMPACTION_SETTLEMENT];
2023
- const abortRecovery = planAbortRecovery({
2024
- messages: result.messages ?? [],
2025
- error: result.error,
2026
- processSignal: result.processSignal,
2027
- sessionAvailable: Boolean(options.sessionFile && existsSync(options.sessionFile)),
2028
- alreadyResumed: recoveryState !== "unused",
2029
- stopped: result.stopped || result.detached || options.signal?.aborted,
2030
- interrupted: result.interrupted || intercomDetached || options.interruptSignal?.aborted,
2031
- timedOut: result.timedOut,
2032
- toolBudgetExhausted: result.toolBudgetBlocked,
2033
- usageBudgetExhausted: false,
2034
- structuredOutputFailed: result.structuredOutputFailed,
2035
- acceptanceFailed: false,
2036
- currentTool: result.progress?.currentTool,
2037
- afterCompactionSettlement,
2038
- });
2039
- if (abortRecovery.action === "resume") {
2040
- recoveryState = "abort-recovery";
2041
- nextAttemptTask = abortRecovery.prompt;
2042
- attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
2043
- continue;
2044
- }
2045
- if (abortRecovery.diagnostic) {
2046
- result.error = result.error ? `${result.error}\n${abortRecovery.diagnostic}` : abortRecovery.diagnostic;
2047
- attempt.error = result.error;
2048
- break modelAttemptsLoop;
2049
- }
2050
- }
2051
- if (recoveringAbort && !attemptSucceeded) break modelAttemptsLoop;
2052
- if (options.workflowChildPermitLaunch && !attemptSucceeded) break modelAttemptsLoop;
2053
- // Preserve the legacy intercom handoff contract: once this logical run has
2054
- // been handed to a supervisor, terminating that attempt must not launch a
2055
- // model fallback. Explicit user detach retains fallback.
2056
- if (intercomDetached || result.timedOut) break modelAttemptsLoop;
2057
- if (attemptSucceeded) break modelAttemptsLoop;
2058
-
2059
- const retryableModelFailure = isRetryableModelFailureAttempt({ error: result.error, messages: result.messages, toolCount: result.progressSummary?.toolCount });
2060
- if (retryableModelFailure) recordRetryableModelFailure(result.model ?? candidate, result.error);
2061
- if (isContextOverflow(result.error)) {
2062
- result.contextOverflow = true;
2063
- attemptNotes.push(`[fallback] ${attempt.model} failed: context overflow — the input exceeds this model's context window. Reduce the task input or use a model with a larger context window.`);
2064
- break modelAttemptsLoop;
2065
- }
2066
- if (!retryableModelFailure || modelIndex === modelsToTry.length - 1) break modelAttemptsLoop;
2067
- attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[modelIndex + 1]));
2068
- break;
1894
+ let recoveryPrompt = task;
1895
+ for (let attemptIndex = 0; attemptIndex < 2; attemptIndex++) {
1896
+ const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
1897
+ const attemptResult = await runSingleAttempt(runtimeCwd, agent, recoveryPrompt, candidate, attemptOptions, {
1898
+ sessionEnabled,
1899
+ systemPrompt,
1900
+ acceptancePrompt,
1901
+ resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
1902
+ skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
1903
+ jsonlPath,
1904
+ artifactPaths: artifactPathsResult,
1905
+ transcriptWriter,
1906
+ attemptNotes,
1907
+ outputSnapshot,
1908
+ originalTask: task,
1909
+ orcaProgressTab,
1910
+ launchWarnings,
1911
+ verifyModel,
1912
+ });
1913
+ lastResult = attemptResult;
1914
+ sumUsage(aggregateUsage, attemptResult.usage);
1915
+ totalToolCount += attemptResult.progressSummary?.toolCount ?? 0;
1916
+ totalDurationMs += attemptResult.progressSummary?.durationMs ?? 0;
1917
+ if (attemptResult.exitCode === 0 && !attemptResult.error) break;
1918
+ const recovery = planAbortRecovery({
1919
+ messages: attemptResult.messages ?? [],
1920
+ error: attemptResult.error,
1921
+ processSignal: attemptResult.processSignal,
1922
+ sessionAvailable: Boolean(options.sessionFile && existsSync(options.sessionFile)),
1923
+ alreadyResumed: attemptIndex > 0,
1924
+ stopped: attemptResult.stopped || attemptResult.detached || Boolean(detachedReason) || Boolean(options.workflowChildPermitLaunch) || options.signal?.aborted,
1925
+ interrupted: attemptResult.interrupted || options.interruptSignal?.aborted,
1926
+ timedOut: attemptResult.timedOut,
1927
+ toolBudgetExhausted: attemptResult.toolBudgetBlocked,
1928
+ usageBudgetExhausted: false,
1929
+ structuredOutputFailed: attemptResult.structuredOutputFailed,
1930
+ acceptanceFailed: false,
1931
+ currentTool: attemptResult.progress?.currentTool,
1932
+ afterCompactionSettlement: (attemptResult as AbortRecoverySingleResult)[AFTER_COMPACTION_SETTLEMENT],
1933
+ });
1934
+ if (recovery.action === "resume") {
1935
+ recoveryPrompt = recovery.prompt;
1936
+ attemptNotes.push("[abort-recovery] compaction abort after useful progress; resuming the retained child session once on the same model.");
1937
+ continue;
1938
+ }
1939
+ if (recovery.diagnostic) {
1940
+ attemptResult.error = attemptResult.error ? `${attemptResult.error}\n${recovery.diagnostic}` : recovery.diagnostic;
2069
1941
  }
1942
+ break;
2070
1943
  }
1944
+ if (!lastResult) throw new Error("Subagent did not produce a result.");
1945
+ if (isContextOverflow(lastResult.error)) lastResult.contextOverflow = true;
2071
1946
 
2072
1947
  const result = withRunContext(lastResult ?? {
2073
1948
  index: options.index ?? 0,
@@ -2081,8 +1956,7 @@ async function runSyncCompletionInner(
2081
1956
  result.task = task;
2082
1957
 
2083
1958
  result.usage = aggregateUsage;
2084
- result.attemptedModels = attemptedModels.length > 0 ? attemptedModels : undefined;
2085
- result.modelAttempts = modelAttempts.length > 0 ? modelAttempts : undefined;
1959
+ result.requestedModel = requestedModel;
2086
1960
  result.progressSummary = {
2087
1961
  ...(childSessionName ? { sessionName: childSessionName } : {}),
2088
1962
  toolCount: totalToolCount,
@@ -2172,7 +2046,6 @@ async function runSyncCompletionInner(
2172
2046
  savedPath: result.savedOutputPath,
2173
2047
  outputReference: result.outputReference,
2174
2048
  }).displayOutput;
2175
- artifactOutputByResult.set(result, result.finalOutput);
2176
2049
  }
2177
2050
  result.error = result.error ? `${result.error}\n${acceptanceFailure}` : acceptanceFailure;
2178
2051
  if (artifactPathsResult && options.artifactConfig?.enabled !== false && options.artifactConfig?.includeOutput !== false) {