@cydm/pie 2.7.11 → 2.7.13

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.
@@ -1,8 +1,10 @@
1
1
  import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
2
2
  import {
3
3
  Agent,
4
+ createAgentOperationError,
5
+ getAgentFailureMetadata,
4
6
  getToolCallerIdentity
5
- } from "./chunk-XVF6V6Q5.js";
7
+ } from "./chunk-HGUYS5SF.js";
6
8
  import {
7
9
  AssistantMessageEventStream,
8
10
  FileSystemGateway,
@@ -12,7 +14,7 @@ import {
12
14
  getPlatformConfig,
13
15
  hashText,
14
16
  streamSimple
15
- } from "./chunk-3VEIP2VY.js";
17
+ } from "./chunk-R46YPPTU.js";
16
18
  import {
17
19
  Type
18
20
  } from "./chunk-XXGYUYXI.js";
@@ -1001,9 +1003,9 @@ var PLAN_POWERSHELL_READ_COMMANDS = new Set([
1001
1003
  "Compare-Object",
1002
1004
  "Test-Path"
1003
1005
  ].map((name) => name.toLowerCase()));
1004
- var PLAN_POWERSHELL_READ_ALIASES = /* @__PURE__ */ new Set(["cat", "ls", "pwd", "echo", "sort"]);
1006
+ var PLAN_POWERSHELL_READ_ALIASES = /* @__PURE__ */ new Set(["cat", "ls", "pwd", "echo"]);
1005
1007
  var PLAN_POWERSHELL_SAFE_PARAMETERS = /^(?:-(?:Raw|TotalCount|Head|Tail|First|Last|Wait|LiteralPath|Path|Name|Pattern|Recurse|Filter|Include|Exclude|SimpleMatch|CaseSensitive|Quiet|Word|Line|Character|Encoding|ErrorAction|WarningAction))$/i;
1006
- var PLAN_POWERSHELL_SEPARATORS = /[;\n\r\t]|[<>]{1,2}|\||&&|\|\|/;
1008
+ var PLAN_POWERSHELL_SEPARATORS = /[;\n\r\t&<>|]/;
1007
1009
  var PLAN_POWERSHELL_NON_ASCII = /[^\x20-\x7E]/;
1008
1010
  var PLAN_POWERSHELL_UNSAFE_TOKENS = /\$\(|`|%\{|[(){}\[\]]|\bsource\b|\.\s+[/~]|\b(bash|sh|zsh|pwsh|powershell|cmd|npm|npx|node|python3?)\b/i;
1009
1011
  function isPlanModeSafePowerShellCommand(command) {
@@ -1331,7 +1333,9 @@ var DEFAULT_COMPACTION_CONFIG = {
1331
1333
  summaryPrefix: SUMMARY_PREFIX
1332
1334
  };
1333
1335
  var APPROX_BYTES_PER_TOKEN = 4;
1334
- var RESIZED_IMAGE_BYTES_ESTIMATE = 7373;
1336
+ var IMAGE_MEDIA_BYTES_ESTIMATE = 7373;
1337
+ var AUDIO_MEDIA_BYTES_ESTIMATE = 6800;
1338
+ var VIDEO_MEDIA_BYTES_ESTIMATE = 16e3;
1335
1339
  function estimateTokens(message) {
1336
1340
  return approximateBytesAsTokens(estimateModelVisibleBytes(message));
1337
1341
  }
@@ -1456,20 +1460,16 @@ function planRetainedTail(summaries, options = {}) {
1456
1460
  if (boundaries.length === 0) {
1457
1461
  return nonePlan("no-boundary");
1458
1462
  }
1459
- const prefix = new Array(summaries.length + 1).fill(0);
1460
- for (let i = 0; i < summaries.length; i++) {
1461
- prefix[i + 1] = prefix[i] + Math.max(0, Math.floor(summaries[i].estTokens || 0));
1462
- }
1463
1463
  const groupEnd = boundaries.map((_, index) => boundaries[index + 1] ?? summaries.length);
1464
1464
  const planFrom = (startIndex, reason) => ({
1465
1465
  retainedTail: { kind: "from-entry", entryId: summaries[startIndex].id },
1466
1466
  entryIds: summaries.slice(startIndex).map((summary) => summary.id),
1467
- tailTokens: prefix[summaries.length] - prefix[startIndex],
1467
+ tailTokens: estimateRetainedTailTokens(summaries, { kind: "from-entry", entryId: summaries[startIndex].id }),
1468
1468
  reason
1469
1469
  });
1470
1470
  let accumulated = 0;
1471
1471
  for (let k = boundaries.length - 1; k >= 0; k--) {
1472
- const groupTokens = prefix[groupEnd[k]] - prefix[boundaries[k]];
1472
+ const groupTokens = summaries.slice(boundaries[k], groupEnd[k]).reduce((sum, summary) => sum + Math.max(0, Math.floor(summary.estTokens || 0)), 0);
1473
1473
  if (groupTokens > budget) {
1474
1474
  if (accumulated === 0) {
1475
1475
  return nonePlan("single-group-over-budget");
@@ -1545,6 +1545,9 @@ function estimateCompactionGain(params) {
1545
1545
  const tokensBefore = Math.max(0, params.tokensBefore);
1546
1546
  const tokenSavings = Math.max(0, tokensBefore - projectedTokens);
1547
1547
  const estimate = { summaryTokens, tailTokens, projectedTokens, tokenSavings };
1548
+ if (!Number.isFinite(projectedTokens)) {
1549
+ return { ...estimate, ok: false, reason: "retained context has unknown model-visible size; supply a compaction modelView" };
1550
+ }
1548
1551
  if (params.budgetTokens !== void 0 && params.budgetTokens > 0 && projectedTokens > params.budgetTokens) {
1549
1552
  return {
1550
1553
  ...estimate,
@@ -1697,12 +1700,7 @@ Return a text-only successor handoff. Do not call any tools.` }],
1697
1700
  ).result();
1698
1701
  throwIfAborted();
1699
1702
  if (response.stopReason === "error" && (!response.content.some((block) => block.type === "toolCall") || classifyRemoteFailure(response.remoteFailure, response.errorMessage ?? "") !== void 0)) {
1700
- const providerError = new Error(response.errorMessage || "Compaction provider error");
1701
- const explicitRetryable = response.retryable;
1702
- if (typeof explicitRetryable === "boolean") providerError.retryable = explicitRetryable;
1703
- if (response.replayEligible === true) providerError.replayEligible = true;
1704
- providerError.remoteFailure = response.remoteFailure;
1705
- throw providerError;
1703
+ throw Object.assign(new Error(response.errorMessage || "Compaction provider error"), getAgentFailureMetadata(response));
1706
1704
  }
1707
1705
  } catch (error) {
1708
1706
  throwIfAborted();
@@ -1825,7 +1823,7 @@ function normalizeUserContentBlock(block) {
1825
1823
  return { type: "text", text: block.text };
1826
1824
  }
1827
1825
  if (block.type === "hostContext") {
1828
- return { type: "text", text: "x".repeat(512) };
1826
+ return { type: "hostContext" };
1829
1827
  }
1830
1828
  if (block.type === "image") {
1831
1829
  return { type: "image", mimeType: block.mimeType };
@@ -1857,22 +1855,16 @@ function estimateMessageMediaBytes(message) {
1857
1855
  }
1858
1856
  function estimateUserContentBlockMediaBytes(block) {
1859
1857
  if (block.type === "image") {
1860
- return estimateImageContentBytes(block);
1858
+ return IMAGE_MEDIA_BYTES_ESTIMATE;
1861
1859
  }
1862
1860
  if (block.type === "audio") {
1863
- return estimateInlineDataBytes(block);
1861
+ return AUDIO_MEDIA_BYTES_ESTIMATE;
1864
1862
  }
1865
1863
  if (block.type === "video") {
1866
- return estimateInlineDataBytes(block);
1864
+ return VIDEO_MEDIA_BYTES_ESTIMATE;
1867
1865
  }
1868
1866
  return 0;
1869
1867
  }
1870
- function estimateImageContentBytes(block) {
1871
- return Math.max(RESIZED_IMAGE_BYTES_ESTIMATE, estimateInlineDataBytes(block));
1872
- }
1873
- function estimateInlineDataBytes(block) {
1874
- return utf8ByteLength(block.data || "");
1875
- }
1876
1868
  function approximateBytesAsTokens(bytes) {
1877
1869
  return Math.ceil(bytes / APPROX_BYTES_PER_TOKEN);
1878
1870
  }
@@ -4623,10 +4615,26 @@ function prepareSessionCompaction(params) {
4623
4615
  const agentOnlyFailure = excludedFailure === null && requireExclusion ? agentTail ?? null : null;
4624
4616
  const summaries = sessionManager.getActiveContextSummaries();
4625
4617
  const entries = sessionManager.getResidentContextEntries();
4626
- const effectiveSummaries = excludedFailure ? summaries.filter((summary) => summary.id !== excludedFailure.entryId) : summaries;
4618
+ const sourceSummaries = excludedFailure ? summaries.filter((summary) => summary.id !== excludedFailure.entryId) : summaries;
4627
4619
  const effectiveEntries = excludedFailure ? entries.filter((entry) => entry.id !== excludedFailure.entryId) : entries;
4628
4620
  const summaryInput = excludedFailure || agentOnlyFailure ? messages.slice(0, -1) : messages;
4629
- const inputTokens = calculateTotalTokens(summaryInput);
4621
+ const estimates = /* @__PURE__ */ new Map();
4622
+ const estimate = (message) => {
4623
+ const cached = estimates.get(message);
4624
+ if (cached !== void 0) return cached;
4625
+ const visible = params.modelView ? params.modelView(message) : message;
4626
+ const unknown = visible.role === "user" && Array.isArray(visible.content) && visible.content.some((block) => block.type === "hostContext");
4627
+ const tokens = unknown ? Infinity : estimateTokens(visible);
4628
+ estimates.set(message, tokens);
4629
+ return tokens;
4630
+ };
4631
+ const bodies = new Map(effectiveEntries.map((entry) => [entry.id, entry]));
4632
+ const effectiveSummaries = sourceSummaries.map((summary) => {
4633
+ const entry = bodies.get(summary.id);
4634
+ return { ...summary, estTokens: entry?.message ? estimate(entry.message) : summary.type === "message" ? Infinity : summary.estTokens };
4635
+ });
4636
+ const inputEstimate = summaryInput.reduce((sum, message) => sum + estimate(message), 0);
4637
+ const inputTokens = Number.isFinite(inputEstimate) ? inputEstimate : calculateTotalTokens(summaryInput);
4630
4638
  const reductionLimit = inputTokens >= 1e3 ? inputTokens - Math.ceil(inputTokens * 0.1) : Infinity;
4631
4639
  const contextLimit = Math.min(
4632
4640
  reductionLimit,
@@ -4640,7 +4648,7 @@ function prepareSessionCompaction(params) {
4640
4648
  let planned = planRetainedTail(effectiveSummaries, { ...params.compactionConfig, keepRecentTokens: tailBudget });
4641
4649
  if (planned.retainedTail.kind !== "none") {
4642
4650
  const validation = validateRetainedTailEntries({ entries: effectiveEntries, retainedTail: planned.retainedTail });
4643
- planned = validation.ok ? { ...planned, entryIds: validation.entryIds, tailTokens: validation.tailTokens } : { retainedTail: { kind: "none" }, entryIds: [], tailTokens: 0, reason: "pairing" };
4651
+ planned = validation.ok ? { ...planned, entryIds: validation.entryIds, tailTokens: estimateRetainedTailTokens(effectiveSummaries, planned.retainedTail) } : { retainedTail: { kind: "none" }, entryIds: [], tailTokens: 0, reason: "pairing" };
4644
4652
  }
4645
4653
  return {
4646
4654
  sessionId: sessionManager.getActiveSessionId(),
@@ -4696,7 +4704,7 @@ function verifyPreparedCompactionCommit(params) {
4696
4704
  const gain = estimateCompactionGain({
4697
4705
  summary: checkpointSummary,
4698
4706
  tailTokens: estimateRetainedTailTokens(params.preparation.summaries, retainedTail),
4699
- tokensBefore: params.result.tokensBefore ?? params.preparation.inputTokens,
4707
+ tokensBefore: params.preparation.inputTokens,
4700
4708
  budgetTokens: params.preparation.budgetTokens
4701
4709
  });
4702
4710
  if (!gain.ok) {
@@ -5176,7 +5184,13 @@ async function preparePromptMessages(params) {
5176
5184
 
5177
5185
  // ../../packages/agent-framework/src/session/controller/pre-request-policy.ts
5178
5186
  async function runPreRequestAutoCompact(params) {
5179
- if (params.disposed || params.runtimeGuardTriggered || params.isStreaming || params.recoveryActive || params.agent.state.messages.length === 0) {
5187
+ const boundary = params.toolContinuation;
5188
+ const cancelled = () => params.isCurrent?.() === false || boundary?.signal.aborted === true;
5189
+ if (boundary) {
5190
+ await boundary.sync();
5191
+ if (cancelled()) return;
5192
+ }
5193
+ if (params.disposed || params.runtimeGuardTriggered || params.isStreaming && !boundary || params.recoveryActive() || params.agent.state.messages.length === 0) {
5180
5194
  return;
5181
5195
  }
5182
5196
  const decision = params.getAutoCompactDecision?.({
@@ -5185,12 +5199,17 @@ async function runPreRequestAutoCompact(params) {
5185
5199
  cwd: params.cwd,
5186
5200
  messages: params.agent.state.messages.slice()
5187
5201
  });
5188
- if (decision?.reason !== "threshold") {
5202
+ if (cancelled() || decision?.reason !== "threshold") {
5189
5203
  return;
5190
5204
  }
5191
5205
  try {
5192
5206
  await params.runAutoCompact();
5193
- } catch {
5207
+ } catch (error) {
5208
+ if (boundary) throw createAgentOperationError(error, {
5209
+ origin: "tool-continuation",
5210
+ operation: "compaction",
5211
+ terminal: true
5212
+ });
5194
5213
  if (params.isCurrent?.() !== false) params.setRecoveryState("none");
5195
5214
  }
5196
5215
  }
@@ -5444,12 +5463,7 @@ var AgentSessionQueueDispatcher = class {
5444
5463
  return false;
5445
5464
  }
5446
5465
  this.options.emit({ type: "queued_turn_dispatched", queuedInput: cloneQueuedInput(dispatched) });
5447
- if (dispatched.deliveredToAgent || this.options.agent.state.messages.at(-1)?.role === "assistant") {
5448
- if (!dispatched.deliveredToAgent) {
5449
- this.options.queue.deliverToAgent(dispatched);
5450
- }
5451
- await this.options.continueTurn();
5452
- } else {
5466
+ if (!dispatched.deliveredToAgent) {
5453
5467
  const prepared = await this.options.preparePromptMessages(dispatched.messages, {
5454
5468
  source: dispatched.source,
5455
5469
  streamingBehavior: dispatched.mode,
@@ -5459,13 +5473,20 @@ var AgentSessionQueueDispatcher = class {
5459
5473
  visibility: dispatched.visibility
5460
5474
  });
5461
5475
  if (generation !== this.generation) return false;
5462
- if (!prepared.handled) {
5463
- if (prepared.systemPrompt !== void 0) {
5464
- this.options.agent.setSystemPrompt(prepared.systemPrompt);
5465
- }
5466
- await this.options.agent.prompt(prepared.messages);
5467
- await this.options.syncNewAgentMessages();
5476
+ if (prepared.handled) return true;
5477
+ if (prepared.systemPrompt !== void 0) {
5478
+ this.options.agent.setSystemPrompt(prepared.systemPrompt);
5468
5479
  }
5480
+ dispatched.messages = prepared.messages;
5481
+ }
5482
+ if (dispatched.deliveredToAgent || this.options.agent.state.messages.at(-1)?.role === "assistant") {
5483
+ if (!dispatched.deliveredToAgent) {
5484
+ this.options.queue.deliverToAgent(dispatched);
5485
+ }
5486
+ await this.options.continueTurn();
5487
+ } else {
5488
+ await this.options.agent.prompt(dispatched.messages);
5489
+ await this.options.syncNewAgentMessages();
5469
5490
  }
5470
5491
  return true;
5471
5492
  }
@@ -5497,7 +5518,8 @@ function isAssistantTerminalCompletion(message) {
5497
5518
  return !message.content.some((block) => block.type === "toolCall");
5498
5519
  }
5499
5520
  function createFailureClassification(context, retry, classifyFailure) {
5500
- if (context.message?.role === "assistant" && context.message.replayEligible === true && context.message.stopReason === "error") {
5521
+ const terminalOperation = context.message?.role === "assistant" && context.message.operationFailure?.terminal === true;
5522
+ if (!terminalOperation && context.message?.role === "assistant" && context.message.replayEligible === true && context.message.stopReason === "error") {
5501
5523
  return {
5502
5524
  kind: "provider_transient",
5503
5525
  retryable: true,
@@ -5508,7 +5530,7 @@ function createFailureClassification(context, retry, classifyFailure) {
5508
5530
  replayExempt: true
5509
5531
  };
5510
5532
  }
5511
- if (context.message?.role === "assistant" && context.message.retryable === false) {
5533
+ if (context.message?.role === "assistant" && (terminalOperation || context.message.retryable === false)) {
5512
5534
  return { kind: "unknown", retryable: false, compactable: false, userAbort: context.message.stopReason === "aborted", terminal: true, message: context.errorMessage };
5513
5535
  }
5514
5536
  const remote = context.message?.role === "assistant" && context.message.stopReason === "error" ? classifyRemoteFailure(context.message.remoteFailure, context.errorMessage) : void 0;
@@ -5586,6 +5608,95 @@ var ReplayStallCircuit = class {
5586
5608
  }
5587
5609
  };
5588
5610
 
5611
+ // ../../packages/agent-framework/src/session/controller/retry-decision.ts
5612
+ function getAgentSessionRetryLimit(retry, failure) {
5613
+ return Math.min(
5614
+ (failure !== void 0 ? retry?.maxRetriesForFailure?.(failure) : void 0) ?? retry?.maxRetries ?? 3,
5615
+ failure?.kind === "provider_confirmation" ? 1 : Infinity
5616
+ );
5617
+ }
5618
+ var AgentSessionRetryEpisode = class {
5619
+ constructor(retry) {
5620
+ this.retry = retry;
5621
+ this.replayCircuit = new ReplayStallCircuit(retry?.systemicReplayStallLimit);
5622
+ }
5623
+ retry;
5624
+ retryAttempts = /* @__PURE__ */ new Map();
5625
+ total = 0;
5626
+ replayCircuit;
5627
+ get totalRetryAttempts() {
5628
+ return this.total;
5629
+ }
5630
+ /** A healthy response ends the current episode and proves endpoint liveness. */
5631
+ succeed() {
5632
+ this.replayCircuit.recordHealthyTurn();
5633
+ const attempts = this.total;
5634
+ this.resetAttempts();
5635
+ return attempts;
5636
+ }
5637
+ /** Cancellation/invalidation resets counters, never the endpoint-health circuit. */
5638
+ resetAttempts() {
5639
+ this.retryAttempts.clear();
5640
+ this.total = 0;
5641
+ }
5642
+ next(failure) {
5643
+ if (!this.retry || !failure.retryable) {
5644
+ if (failure.kind !== "unknown" && failure.kind !== "internal_error") {
5645
+ this.replayCircuit.recordHealthyTurn();
5646
+ }
5647
+ const totalRetryAttempts2 = this.total;
5648
+ this.resetAttempts();
5649
+ return { action: "terminal", failure, totalRetryAttempts: totalRetryAttempts2 };
5650
+ }
5651
+ if (failure.replayExempt === true && this.replayCircuit.isOpen) {
5652
+ const openFailure = asTerminalFailure(
5653
+ failure,
5654
+ `${failure.message} (replay circuit open: ${this.replayCircuit.consecutiveTurnStalls} consecutive turns ended by timeout-replay exhaustion)`
5655
+ );
5656
+ const totalRetryAttempts2 = this.total;
5657
+ this.resetAttempts();
5658
+ return { action: "exhausted", failure: openFailure, maxRetries: 0, totalRetryAttempts: totalRetryAttempts2, reason: "circuit_open" };
5659
+ }
5660
+ const maxRetries = getAgentSessionRetryLimit(this.retry, failure);
5661
+ const kindAttempts = (this.retryAttempts.get(failure.kind) ?? 0) + 1;
5662
+ this.retryAttempts.set(failure.kind, kindAttempts);
5663
+ this.total += 1;
5664
+ const totalRetryAttempts = this.total;
5665
+ const totalBudget = this.retry.maxTotalRetries;
5666
+ const kindExhausted = kindAttempts > maxRetries;
5667
+ const totalExhausted = totalBudget !== void 0 && totalRetryAttempts > totalBudget;
5668
+ const delayMs = resolveRetryDelay(
5669
+ (this.retry.baseDelayMs ?? 2e3) * 2 ** (kindAttempts - 1),
5670
+ this.retry.maxDelayMs,
5671
+ failure.retryAfterMs
5672
+ );
5673
+ if (kindExhausted || totalExhausted || delayMs === void 0) {
5674
+ const terminalFailure = asTerminalFailure(
5675
+ failure,
5676
+ delayMs === void 0 ? `${failure.message} (Retry-After exceeds the allowed wait; automatic recovery paused)` : failure.message
5677
+ );
5678
+ const circuit = failure.replayExempt === true ? this.replayCircuit.recordReplayExhaustion() : void 0;
5679
+ this.resetAttempts();
5680
+ return {
5681
+ action: "exhausted",
5682
+ failure: terminalFailure,
5683
+ maxRetries: totalExhausted ? totalBudget : maxRetries,
5684
+ totalRetryAttempts,
5685
+ reason: delayMs === void 0 ? "retry_after" : totalExhausted ? "total_budget" : "kind_budget",
5686
+ ...circuit ? { circuit } : {}
5687
+ };
5688
+ }
5689
+ return {
5690
+ action: "retry",
5691
+ failure,
5692
+ attempt: kindAttempts,
5693
+ maxRetries,
5694
+ delayMs,
5695
+ totalRetryAttempts
5696
+ };
5697
+ }
5698
+ };
5699
+
5589
5700
  // ../../packages/agent-framework/src/session/controller/retry-policy.ts
5590
5701
  function defaultSetTimeout4(callback, delay) {
5591
5702
  return setTimeout(callback, delay);
@@ -5600,21 +5711,14 @@ var AgentSessionRetryController = class {
5600
5711
  this.removeTrailingAssistantError = removeTrailingAssistantError2;
5601
5712
  this.continueTurn = continueTurn;
5602
5713
  this.setRecoveryState = setRecoveryState;
5603
- this.replayCircuit = new ReplayStallCircuit(retry?.systemicReplayStallLimit);
5714
+ this.episode = new AgentSessionRetryEpisode(retry);
5604
5715
  }
5605
5716
  retry;
5606
5717
  emit;
5607
5718
  removeTrailingAssistantError;
5608
5719
  continueTurn;
5609
5720
  setRecoveryState;
5610
- /** Attempts consumed per failure KIND: budgets are granted per kind (a
5611
- * 10-attempt network budget must not be eaten by interleaved provider
5612
- * errors, and vice versa), so each kind counts its own attempts. */
5613
- retryAttempts = /* @__PURE__ */ new Map();
5614
- /** Total attempts across kinds in this recovery episode, for reporting. */
5615
- totalRetryAttempts = 0;
5616
- /** L2 systemic-stall circuit (see replay-stall-circuit.ts). */
5617
- replayCircuit;
5721
+ episode;
5618
5722
  retryTimer;
5619
5723
  retryEpoch = 0;
5620
5724
  pendingRetry;
@@ -5628,72 +5732,45 @@ var AgentSessionRetryController = class {
5628
5732
  waitForRetry() {
5629
5733
  return this.pendingRetry ?? Promise.resolve();
5630
5734
  }
5631
- /** L2 circuit: healthy turn (success or an actively-answered failure). */
5632
- noteEndpointHealthy() {
5633
- this.replayCircuit.recordHealthyTurn();
5634
- }
5635
5735
  async handleTurnEnd(message, failure) {
5636
5736
  if (!this.retry || !message || message.role !== "assistant") {
5637
5737
  this.settleStrandedRetry(failure);
5638
5738
  return;
5639
5739
  }
5640
5740
  if (!failure) {
5641
- this.noteEndpointHealthy();
5642
- if (this.totalRetryAttempts > 0) {
5643
- this.emit({ type: "retry_succeeded", attempts: this.totalRetryAttempts });
5644
- this.emit({ type: "turn_retry_succeeded", attempts: this.totalRetryAttempts });
5645
- this.resetAttempts();
5741
+ const attempts = this.episode.succeed();
5742
+ if (attempts > 0) {
5743
+ this.emit({ type: "retry_succeeded", attempts });
5744
+ this.emit({ type: "turn_retry_succeeded", attempts });
5646
5745
  this.setRecoveryState("none");
5647
5746
  this.resolvePending();
5648
5747
  }
5649
5748
  return;
5650
5749
  }
5651
- if (!failure.retryable) {
5652
- if (failure.kind !== "unknown" && failure.kind !== "internal_error") {
5653
- this.noteEndpointHealthy();
5654
- }
5655
- if (this.totalRetryAttempts > 0) {
5656
- this.resetAttempts();
5750
+ const decision = this.episode.next(failure);
5751
+ if (decision.action === "terminal") {
5752
+ if (decision.totalRetryAttempts > 0) {
5657
5753
  this.setRecoveryState("none");
5658
5754
  this.resolvePending();
5659
5755
  }
5660
5756
  return;
5661
5757
  }
5662
- if (failure.replayExempt === true && this.replayCircuit.isOpen) {
5663
- const openFailure = asTerminalFailure(failure, `${failure.message} (replay circuit open: ${this.replayCircuit.consecutiveTurnStalls} consecutive turns ended by timeout-replay exhaustion)`);
5664
- this.emit({ type: "retry_exhausted", errorMessage: openFailure.message, maxRetries: 0 });
5665
- this.emit({ type: "turn_failed_terminal", failure: openFailure, message });
5666
- this.emit({ type: "turn_settled" });
5667
- this.resetAttempts();
5668
- this.setRecoveryState("terminal");
5669
- this.resolvePending();
5670
- return;
5671
- }
5672
- const maxRetries = Math.min(this.retry.maxRetriesForFailure?.(failure) ?? this.retry.maxRetries ?? 3, failure.kind === "provider_confirmation" ? 1 : Infinity);
5673
- const kindAttempts = (this.retryAttempts.get(failure.kind) ?? 0) + 1;
5674
- this.retryAttempts.set(failure.kind, kindAttempts);
5675
- this.totalRetryAttempts += 1;
5676
- const totalBudget = this.retry.maxTotalRetries;
5677
- const kindExhausted = kindAttempts > maxRetries;
5678
- const totalExhausted = totalBudget !== void 0 && this.totalRetryAttempts > totalBudget;
5679
- const delayMs = resolveRetryDelay((this.retry.baseDelayMs ?? 2e3) * 2 ** (kindAttempts - 1), this.retry.maxDelayMs, failure.retryAfterMs);
5680
- if (kindExhausted || totalExhausted || delayMs === void 0) {
5681
- const terminalFailure = asTerminalFailure(failure, delayMs === void 0 ? `${failure.message} (Retry-After exceeds the allowed wait; automatic recovery paused)` : failure.message);
5682
- if (failure.replayExempt === true) {
5683
- const outcome = this.replayCircuit.recordReplayExhaustion();
5684
- if (outcome.opened) {
5685
- this.emit({ type: "retry_exhausted", errorMessage: `systemic timeout-stall: ${outcome.consecutive} consecutive turns ended by timeout-replay exhaustion`, maxRetries: totalExhausted ? totalBudget : maxRetries });
5686
- }
5758
+ if (decision.action === "exhausted") {
5759
+ if (decision.circuit?.opened) {
5760
+ this.emit({
5761
+ type: "retry_exhausted",
5762
+ errorMessage: `systemic timeout-stall: ${decision.circuit.consecutive} consecutive turns ended by timeout-replay exhaustion`,
5763
+ maxRetries: decision.maxRetries
5764
+ });
5687
5765
  }
5688
- this.emit({ type: "retry_exhausted", errorMessage: terminalFailure.message, maxRetries: totalExhausted ? totalBudget : maxRetries });
5689
- this.emit({ type: "turn_failed_terminal", failure: terminalFailure, message });
5766
+ this.emit({ type: "retry_exhausted", errorMessage: decision.failure.message, maxRetries: decision.maxRetries });
5767
+ this.emit({ type: "turn_failed_terminal", failure: decision.failure, message });
5690
5768
  this.emit({ type: "turn_settled" });
5691
- this.resetAttempts();
5692
5769
  this.setRecoveryState("terminal");
5693
5770
  this.resolvePending();
5694
5771
  return;
5695
5772
  }
5696
- const attempt = kindAttempts;
5773
+ const { attempt, maxRetries, delayMs } = decision;
5697
5774
  this.ensurePending();
5698
5775
  const epoch = this.retryEpoch;
5699
5776
  const pending = this.pendingRetry;
@@ -5720,7 +5797,7 @@ var AgentSessionRetryController = class {
5720
5797
  this.emit({ type: "retry_exhausted", errorMessage, maxRetries });
5721
5798
  this.emit({ type: "turn_failed_terminal", failure: terminalFailure, message });
5722
5799
  this.emit({ type: "turn_settled" });
5723
- this.resetAttempts();
5800
+ this.episode.resetAttempts();
5724
5801
  this.setRecoveryState("terminal");
5725
5802
  this.resolvePending(pending);
5726
5803
  });
@@ -5745,24 +5822,20 @@ var AgentSessionRetryController = class {
5745
5822
  userAbort: false,
5746
5823
  terminal: true
5747
5824
  });
5748
- const maxRetries = Math.min((failure !== void 0 ? this.retry?.maxRetriesForFailure?.(failure) : void 0) ?? this.retry?.maxRetries ?? 3, failure?.kind === "provider_confirmation" ? 1 : Infinity);
5825
+ const maxRetries = getAgentSessionRetryLimit(this.retry, failure);
5749
5826
  this.emit({ type: "retry_exhausted", errorMessage: terminalFailure.message, maxRetries });
5750
5827
  this.emit({ type: "turn_failed_terminal", failure: terminalFailure });
5751
5828
  this.emit({ type: "turn_settled" });
5752
- this.resetAttempts();
5829
+ this.episode.resetAttempts();
5753
5830
  this.setRecoveryState("terminal");
5754
5831
  this.resolvePending();
5755
5832
  }
5756
5833
  cancel() {
5757
5834
  this.retryEpoch++;
5758
5835
  this.cancelTimerOnly();
5759
- this.resetAttempts();
5836
+ this.episode.resetAttempts();
5760
5837
  this.resolvePending();
5761
5838
  }
5762
- resetAttempts() {
5763
- this.retryAttempts.clear();
5764
- this.totalRetryAttempts = 0;
5765
- }
5766
5839
  cancelTimerOnly() {
5767
5840
  if (this.retryTimer === void 0) {
5768
5841
  return;
@@ -6101,7 +6174,8 @@ async function runAgentSessionCompaction(params) {
6101
6174
  const preparation = prepareSessionCompaction({
6102
6175
  sessionManager: params.sessionManager,
6103
6176
  messages: params.agent.state.messages,
6104
- budgetTokens: params.compactionBudgetTokens?.()
6177
+ budgetTokens: params.compactionBudgetTokens?.(),
6178
+ modelView: params.compaction.modelView
6105
6179
  });
6106
6180
  let result = await params.compaction.compact({
6107
6181
  reason: params.reason,
@@ -6239,7 +6313,8 @@ async function handleAgentSessionTurnEndRuntimePolicies(params) {
6239
6313
  params.emit({ type: "turn_settled" });
6240
6314
  return;
6241
6315
  }
6242
- const compactDecision = params.getAutoCompactDecision?.({
6316
+ const recoveryClosed = failure?.terminal && params.message?.role === "assistant" && (params.message.retryable === false || params.message.operationFailure?.terminal === true);
6317
+ const compactDecision = recoveryClosed ? void 0 : params.getAutoCompactDecision?.({
6243
6318
  agent: params.agent,
6244
6319
  sessionManager: params.sessionManager,
6245
6320
  cwd: params.cwd,
@@ -6380,6 +6455,8 @@ var AgentSessionController = class {
6380
6455
  autoContinue;
6381
6456
  unsubscribeAgent;
6382
6457
  unsubscribeSemantic;
6458
+ unsubscribeToolContinuation;
6459
+ compactionOwner;
6383
6460
  lastSyncedMessageCount = 0;
6384
6461
  disposed = false;
6385
6462
  transitioning = false;
@@ -6483,6 +6560,7 @@ var AgentSessionController = class {
6483
6560
  });
6484
6561
  this.syncFromActiveSession();
6485
6562
  this.unsubscribeAgent = this.agent.subscribe((event) => this.handleAgentEvent(event));
6563
+ this.unsubscribeToolContinuation = this.agent.setBeforeToolContinuation((signal) => this.runPreRequestAutoCompactIfNeeded(signal));
6486
6564
  this.unsubscribeSemantic = this.agent.subscribeSemantic((event) => {
6487
6565
  this.emit({ type: "semantic_event", event });
6488
6566
  });
@@ -6533,6 +6611,7 @@ var AgentSessionController = class {
6533
6611
  this.transitioning = true;
6534
6612
  try {
6535
6613
  this.generation++;
6614
+ if (this.isStreaming) this.agent.resetProcessingState();
6536
6615
  this.cancelScheduledWork();
6537
6616
  this.recoveryState = "none";
6538
6617
  this.pendingCompactionFailure = void 0;
@@ -6664,48 +6743,61 @@ var AgentSessionController = class {
6664
6743
  const generation = this.generation;
6665
6744
  if (this.disposed || this.transitioning) throw new Error("Controller disposed or transitioning");
6666
6745
  const isCurrent = () => generation === this.generation && !this.disposed;
6667
- return this.convergence.run(() => runAgentSessionControllerAutoCompact({
6668
- isCurrent,
6669
- reason,
6670
- willRetry,
6671
- signal: options?.signal,
6672
- compaction: this.compaction,
6673
- agent: this.agent,
6674
- sessionManager: this.sessionManager,
6675
- cwd: this.cwd,
6676
- pendingFailure: this.pendingCompactionFailure,
6677
- emit: (event) => {
6678
- if (isCurrent()) this.emit(event);
6679
- },
6680
- setRecoveryState: (state) => {
6681
- this.recoveryState = state;
6682
- },
6683
- getRecoveryState: () => this.recoveryState,
6684
- getDisposed: () => this.disposed,
6685
- cancelRetry: () => this.retryController.cancel(),
6686
- clearPendingFailure: () => {
6687
- this.pendingCompactionFailure = void 0;
6688
- },
6689
- getCompactionBudgetTokens: this.compactionSettings.getCompactionBudgetTokens,
6690
- removeTrailingAssistantError: () => this.removeTrailingAssistantError(),
6691
- replaceMessagesAndMarkSynced: (messages) => this.replaceMessagesAndMarkSynced(messages),
6692
- markSyncedToAgentState: () => this.markSyncedToAgentState(),
6693
- continueTurn: () => this.continue(),
6694
- handleAgentEndHook: (messages) => this.handleAgentEndHook(messages),
6695
- emitSettled: options?.emitSettled,
6696
- runAfterAgentEndHook: options?.runAfterAgentEndHook,
6697
- cancelOnAbort: options?.cancelOnAbort
6698
- }));
6746
+ if (this.compactionOwner) throw new Error("Compaction already running");
6747
+ const owner = this.compactionOwner = {};
6748
+ try {
6749
+ return await this.convergence.run(() => runAgentSessionControllerAutoCompact({
6750
+ isCurrent,
6751
+ reason,
6752
+ willRetry,
6753
+ signal: options?.signal,
6754
+ compaction: this.compaction,
6755
+ agent: this.agent,
6756
+ sessionManager: this.sessionManager,
6757
+ cwd: this.cwd,
6758
+ pendingFailure: this.pendingCompactionFailure,
6759
+ emit: (event) => {
6760
+ if (isCurrent()) this.emit(event);
6761
+ },
6762
+ setRecoveryState: (state) => {
6763
+ this.recoveryState = state;
6764
+ },
6765
+ getRecoveryState: () => this.recoveryState,
6766
+ getDisposed: () => this.disposed,
6767
+ cancelRetry: () => this.retryController.cancel(),
6768
+ clearPendingFailure: () => {
6769
+ this.pendingCompactionFailure = void 0;
6770
+ },
6771
+ getCompactionBudgetTokens: this.compactionSettings.getCompactionBudgetTokens,
6772
+ removeTrailingAssistantError: () => this.removeTrailingAssistantError(),
6773
+ replaceMessagesAndMarkSynced: (messages) => this.replaceMessagesAndMarkSynced(messages),
6774
+ markSyncedToAgentState: () => this.markSyncedToAgentState(),
6775
+ continueTurn: async () => {
6776
+ if (!isCurrent() || options?.signal?.aborted) return;
6777
+ if (this.compactionOwner === owner) this.compactionOwner = void 0;
6778
+ this.recoveryState = "none";
6779
+ await this.continue();
6780
+ },
6781
+ handleAgentEndHook: (messages) => this.handleAgentEndHook(messages),
6782
+ emitSettled: options?.emitSettled,
6783
+ runAfterAgentEndHook: options?.runAfterAgentEndHook,
6784
+ cancelOnAbort: options?.cancelOnAbort
6785
+ }));
6786
+ } finally {
6787
+ if (this.compactionOwner === owner) this.compactionOwner = void 0;
6788
+ }
6699
6789
  }
6700
6790
  dispose() {
6701
6791
  if (this.disposed) return;
6702
6792
  this.disposed = true;
6793
+ if (this.isStreaming) this.agent.resetProcessingState();
6703
6794
  this.agent.releaseTransport?.();
6704
6795
  this.generation++;
6705
6796
  this.pendingEndOperations.clear();
6706
6797
  this.cancelScheduledWork();
6707
6798
  this.unsubscribeAgent?.();
6708
6799
  this.unsubscribeSemantic?.();
6800
+ this.unsubscribeToolContinuation?.();
6709
6801
  this.queue.clear();
6710
6802
  this.convergence.invalidate("dispose");
6711
6803
  this.listeners.clear();
@@ -6729,19 +6821,26 @@ var AgentSessionController = class {
6729
6821
  });
6730
6822
  }
6731
6823
  }
6732
- async runPreRequestAutoCompactIfNeeded() {
6824
+ async runPreRequestAutoCompactIfNeeded(signal) {
6733
6825
  const generation = this.generation;
6734
6826
  return runPreRequestAutoCompact({
6735
6827
  isCurrent: () => generation === this.generation && !this.disposed,
6736
6828
  disposed: this.disposed,
6737
6829
  runtimeGuardTriggered: this.runtimeGuard.triggered,
6738
6830
  isStreaming: this.isStreaming,
6739
- recoveryActive: this.isRecoveryActive(),
6831
+ recoveryActive: () => this.isRecoveryActive(),
6740
6832
  agent: this.agent,
6741
6833
  sessionManager: this.sessionManager,
6742
6834
  cwd: this.cwd,
6743
6835
  getAutoCompactDecision: this.compactionSettings.getAutoCompactDecision,
6744
- runAutoCompact: () => this.runAutoCompact("threshold", false, { emitSettled: false, runAfterAgentEndHook: false, cancelOnAbort: false }).then(() => void 0),
6836
+ toolContinuation: signal ? { signal, sync: async () => {
6837
+ await this.pendingEndOperations.wait();
6838
+ if (generation === this.generation && !this.disposed && !signal.aborted) await this.syncNewAgentMessages();
6839
+ } } : void 0,
6840
+ runAutoCompact: async () => {
6841
+ const result = await this.runAutoCompact("threshold", false, { signal, emitSettled: false, runAfterAgentEndHook: false, cancelOnAbort: false });
6842
+ if (signal && (!result || result.aborted)) throw new Error("Tool-continuation compaction was aborted");
6843
+ },
6745
6844
  setRecoveryState: (state) => {
6746
6845
  this.recoveryState = state;
6747
6846
  }
@@ -6886,6 +6985,7 @@ var AgentSessionController = class {
6886
6985
  return this.recoveryState !== "none" || this.retryController.hasPendingRetry || this.autoCompact.hasPendingAutoCompact;
6887
6986
  }
6888
6987
  cancelScheduledWork() {
6988
+ this.compactionOwner = void 0;
6889
6989
  this.retryController.cancel();
6890
6990
  this.autoCompact.cancel();
6891
6991
  this.queueDispatcher.cancel();
@@ -8241,6 +8341,9 @@ function withSubagentRunContext(stream, hooks) {
8241
8341
  ${SUBAGENT_FINALIZATION_HINT}` } : context, options);
8242
8342
  };
8243
8343
  }
8344
+ function isRetryableSubagentFailure(message) {
8345
+ return isRetryableSubagentFailureFrom({ errorMessage: message });
8346
+ }
8244
8347
  function isRetryableSubagentFailureFrom(facts) {
8245
8348
  if (facts.retryable === false) return false;
8246
8349
  if (facts.replayClass === "stream_lost" || facts.replayClass === "stream_lost_partial") return true;
@@ -8262,90 +8365,366 @@ function cancellableBackoff(ms, signal) {
8262
8365
  if (signal?.aborted) finish();
8263
8366
  });
8264
8367
  }
8368
+ function factsFromMessage(message) {
8369
+ return {
8370
+ errorMessage: message.errorMessage,
8371
+ retryable: message.retryable,
8372
+ replayClass: message.replayClass,
8373
+ replayEligible: message.replayEligible,
8374
+ remoteFailure: message.remoteFailure,
8375
+ stopReason: message.stopReason
8376
+ };
8377
+ }
8265
8378
  function factsOf(error) {
8266
8379
  if (error instanceof Error) {
8267
8380
  const carrier = error;
8268
- return { errorMessage: error.message, retryable: carrier.retryable, replayClass: carrier.replayClass };
8381
+ return {
8382
+ errorMessage: carrier.subagentFailure?.errorMessage ?? error.message,
8383
+ retryable: carrier.subagentFailure?.retryable ?? carrier.retryable,
8384
+ replayClass: carrier.subagentFailure?.replayClass ?? carrier.replayClass,
8385
+ replayEligible: carrier.subagentFailure?.replayEligible ?? carrier.replayEligible,
8386
+ remoteFailure: carrier.subagentFailure?.remoteFailure ?? carrier.remoteFailure,
8387
+ stopReason: carrier.subagentFailure?.stopReason ?? carrier.stopReason ?? (carrier.subagentFailure?.classification?.userAbort ? "aborted" : "error")
8388
+ };
8269
8389
  }
8270
- return { errorMessage: String(error) };
8390
+ return { errorMessage: String(error), stopReason: "error" };
8271
8391
  }
8272
- function withSubagentResponseRecovery(stream, backoffMs = 2e3, onRetry, onActivity, hooks) {
8392
+ function assistantFromFacts(facts) {
8393
+ return {
8394
+ role: "assistant",
8395
+ api: "unknown",
8396
+ provider: "unknown",
8397
+ model: "unknown",
8398
+ content: [],
8399
+ timestamp: 0,
8400
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
8401
+ stopReason: facts.stopReason ?? "error",
8402
+ errorMessage: facts.errorMessage,
8403
+ ...facts.retryable !== void 0 ? { retryable: facts.retryable } : {},
8404
+ ...facts.replayClass !== void 0 ? { replayClass: facts.replayClass } : {},
8405
+ ...facts.replayEligible !== void 0 ? { replayEligible: facts.replayEligible } : {},
8406
+ ...facts.remoteFailure !== void 0 ? { remoteFailure: facts.remoteFailure } : {}
8407
+ };
8408
+ }
8409
+ function boundedClassification(failure) {
8410
+ return { ...failure, message: failure.message.slice(0, 2e3) };
8411
+ }
8412
+ function failureDetails(facts, classification, recovery) {
8413
+ return {
8414
+ errorMessage: (classification.message || facts.errorMessage || "Subagent response failed").slice(0, 2e3),
8415
+ ...facts.retryable !== void 0 ? { retryable: facts.retryable } : {},
8416
+ ...facts.replayClass !== void 0 ? { replayClass: facts.replayClass } : {},
8417
+ ...facts.replayEligible !== void 0 ? { replayEligible: facts.replayEligible } : {},
8418
+ ...facts.remoteFailure !== void 0 ? {
8419
+ remoteFailure: {
8420
+ category: facts.remoteFailure.category,
8421
+ ...typeof facts.remoteFailure.retryAfterMs === "number" && Number.isFinite(facts.remoteFailure.retryAfterMs) && facts.remoteFailure.retryAfterMs >= 0 ? { retryAfterMs: facts.remoteFailure.retryAfterMs } : {}
8422
+ }
8423
+ } : {},
8424
+ ...facts.stopReason === "aborted" ? { stopReason: facts.stopReason } : {},
8425
+ classification: boundedClassification(classification),
8426
+ recovery
8427
+ };
8428
+ }
8429
+ function legacyRecovery(backoffMs) {
8430
+ return {
8431
+ policy: {
8432
+ isRetryableError: (message) => isRetryableSubagentFailure(message),
8433
+ maxRetries: 2,
8434
+ baseDelayMs: backoffMs
8435
+ },
8436
+ classifyFailure: (context) => {
8437
+ const facts = context.message?.role === "assistant" ? factsFromMessage(context.message) : { errorMessage: context.errorMessage, stopReason: "error" };
8438
+ const retryable = isRetryableSubagentFailureFrom({ ...facts, errorMessage: context.errorMessage });
8439
+ return {
8440
+ kind: retryable ? "provider_transient" : "unknown",
8441
+ retryable,
8442
+ compactable: false,
8443
+ userAbort: false,
8444
+ terminal: !retryable,
8445
+ message: context.errorMessage
8446
+ };
8447
+ }
8448
+ };
8449
+ }
8450
+ function resolveRecovery(options, backoffMs) {
8451
+ if (!options) return legacyRecovery(backoffMs);
8452
+ const fallback = legacyRecovery(backoffMs);
8453
+ return {
8454
+ policy: options.policy ?? fallback.policy,
8455
+ // An explicitly supplied policy without a classifier must reach its own
8456
+ // isRetryableError predicate, exactly as in the main controller. Only the
8457
+ // no-host compatible default installs the legacy prose classifier.
8458
+ classifyFailure: options.classifyFailure ?? (options.policy ? void 0 : fallback.classifyFailure)
8459
+ };
8460
+ }
8461
+ function classify(facts, recovery) {
8462
+ const message = assistantFromFacts(facts);
8463
+ return createFailureClassification({
8464
+ message,
8465
+ errorMessage: facts.errorMessage ?? "Subagent response failed",
8466
+ phase: "stream"
8467
+ }, recovery.policy, recovery.classifyFailure);
8468
+ }
8469
+ var DEFAULT_ITERATOR_CLEANUP_MS = 2e3;
8470
+ async function closeIterator(iterator, hooks, signal) {
8471
+ const cleanup = Promise.resolve().then(() => iterator.return?.()).then(() => void 0).catch(() => void 0);
8472
+ const remaining = hooks?.hardDeadlineAt === void 0 ? DEFAULT_ITERATOR_CLEANUP_MS : Math.max(0, hooks.hardDeadlineAt - Date.now());
8473
+ const waitMs = Math.min(DEFAULT_ITERATOR_CLEANUP_MS, remaining);
8474
+ if (waitMs <= 0 || signal?.aborted) return;
8475
+ let timer;
8476
+ let onAbort;
8477
+ try {
8478
+ await Promise.race([
8479
+ cleanup,
8480
+ new Promise((resolve2) => {
8481
+ timer = setTimeout(resolve2, waitMs);
8482
+ onAbort = () => resolve2();
8483
+ signal?.addEventListener("abort", onAbort, { once: true });
8484
+ })
8485
+ ]);
8486
+ } finally {
8487
+ if (timer !== void 0) clearTimeout(timer);
8488
+ if (onAbort) signal?.removeEventListener("abort", onAbort);
8489
+ }
8490
+ }
8491
+ function singleResponse(message) {
8492
+ const result = new AssistantMessageEventStream();
8493
+ if (message.stopReason === "error" || message.stopReason === "aborted") {
8494
+ result.push({ type: "error", reason: message.stopReason, error: { ...message, content: [] } });
8495
+ } else {
8496
+ result.push({ type: "done", reason: message.stopReason, message });
8497
+ }
8498
+ result.end();
8499
+ return result;
8500
+ }
8501
+ function throwWithDetails(error, details) {
8502
+ const failure = new Error(details.classification?.message ?? (error instanceof Error ? error.message : String(error)));
8503
+ failure.cause = error;
8504
+ failure.subagentFailure = details;
8505
+ throw failure;
8506
+ }
8507
+ function withSubagentResponseRecovery(stream, backoffMs = 2e3, onRetry, onActivity, hooks, recoveryOptions) {
8508
+ const recovery = resolveRecovery(recoveryOptions, backoffMs);
8509
+ const episode = recoveryOptions?.episode ?? hooks?.recoveryEpisode ?? new AgentSessionRetryEpisode(recovery.policy);
8510
+ if (hooks) hooks.recoveryEpisode = episode;
8273
8511
  return async (model, context, options) => {
8274
- for (let attempt = 0; ; attempt++) {
8275
- if (hooks) {
8276
- hooks.terminalText = void 0;
8277
- hooks.terminalStopReason = void 0;
8278
- }
8279
- if (options?.signal?.aborted) throw new Error("Subagent aborted by parent");
8280
- if (hooks?.hardDeadlineAt !== void 0 && Date.now() >= hooks.hardDeadlineAt) throw new Error("Subagent hard deadline reached");
8281
- let message;
8282
- const finishUsage = hooks?.usage ? beginSubagentUsage(hooks.usage, model) : void 0;
8283
- try {
8284
- const response = await stream(model, context, options);
8285
- let terminal;
8286
- const iterator = response[Symbol.asyncIterator]();
8512
+ let attempts = 0;
8513
+ let retries = 0;
8514
+ if (hooks) hooks.failure = void 0;
8515
+ try {
8516
+ for (; ; ) {
8517
+ if (hooks) {
8518
+ hooks.terminalText = void 0;
8519
+ hooks.terminalStopReason = void 0;
8520
+ }
8521
+ if (options?.signal?.aborted) {
8522
+ episode.resetAttempts();
8523
+ throw new Error("Subagent aborted by parent");
8524
+ }
8525
+ if (hooks?.hardDeadlineAt !== void 0 && Date.now() >= hooks.hardDeadlineAt) throw new Error("Subagent hard deadline reached");
8526
+ let message;
8527
+ const finishUsage = hooks?.usage ? beginSubagentUsage(hooks.usage, model) : void 0;
8528
+ attempts += 1;
8287
8529
  try {
8288
- while (true) {
8289
- const next = await iterator.next();
8290
- if (next.done) break;
8291
- try {
8292
- onActivity?.(next.value.type);
8293
- } catch {
8294
- }
8295
- if (next.value.type === "done") {
8296
- terminal = next.value.message;
8297
- finishUsage?.(terminal);
8298
- }
8299
- if (next.value.type === "error") {
8300
- terminal = next.value.error;
8301
- finishUsage?.(terminal);
8302
- }
8303
- if (hooks && (next.value.type === "done" || next.value.type === "error")) {
8304
- const reportTerminal = terminal && ["stop", "length", "error", "aborted"].includes(terminal.stopReason) && !terminal.content.some((block) => block.type === "toolCall");
8305
- hooks.terminalStopReason = reportTerminal && terminal ? terminal.stopReason : void 0;
8306
- hooks.terminalText = reportTerminal && terminal ? terminal.content.filter((block) => block.type === "text").map((block) => block.text).join("\n").slice(0, 24001) : void 0;
8530
+ const response = await stream(model, context, options);
8531
+ let terminal;
8532
+ const iterator = response[Symbol.asyncIterator]();
8533
+ try {
8534
+ for (; ; ) {
8535
+ const next = await iterator.next();
8536
+ if (next.done) break;
8537
+ try {
8538
+ onActivity?.(next.value.type);
8539
+ } catch {
8540
+ }
8541
+ if (next.value.type === "done") {
8542
+ terminal = next.value.message;
8543
+ finishUsage?.(terminal);
8544
+ }
8545
+ if (next.value.type === "error") {
8546
+ terminal = next.value.error;
8547
+ finishUsage?.(terminal);
8548
+ }
8549
+ if (next.value.type === "done" || next.value.type === "error") {
8550
+ if (hooks && terminal) {
8551
+ const reportTerminal = ["stop", "length", "error", "aborted"].includes(terminal.stopReason) && !terminal.content.some((block) => block.type === "toolCall");
8552
+ hooks.terminalStopReason = reportTerminal ? terminal.stopReason : void 0;
8553
+ hooks.terminalText = reportTerminal ? terminal.content.filter((block) => block.type === "text").map((block) => block.text).join("\n").slice(0, 24001) : void 0;
8554
+ }
8555
+ break;
8556
+ }
8307
8557
  }
8558
+ } finally {
8559
+ await closeIterator(iterator, hooks, options?.signal);
8308
8560
  }
8309
- } finally {
8310
- try {
8311
- await iterator.return?.();
8312
- } catch {
8561
+ if (!terminal) throw new Error("Provider protocol error: response ended without a terminal event");
8562
+ message = terminal;
8563
+ } catch (error) {
8564
+ finishUsage?.();
8565
+ if (options?.signal?.aborted) {
8566
+ episode.resetAttempts();
8567
+ throw error;
8313
8568
  }
8314
- }
8315
- if (!terminal) throw new Error("Provider protocol error: response ended without a terminal event");
8316
- message = terminal;
8317
- } catch (error) {
8318
- finishUsage?.();
8319
- if (!options?.signal?.aborted && attempt < 2 && isRetryableSubagentFailureFrom(factsOf(error))) {
8320
- try {
8321
- onRetry?.();
8322
- } catch {
8569
+ const facts2 = factsOf(error);
8570
+ if (facts2.stopReason === "aborted") {
8571
+ episode.resetAttempts();
8572
+ const classification3 = {
8573
+ kind: "user_abort",
8574
+ retryable: false,
8575
+ compactable: false,
8576
+ userAbort: true,
8577
+ terminal: true,
8578
+ message: facts2.errorMessage ?? "Subagent aborted"
8579
+ };
8580
+ const details = failureDetails(facts2, classification3, { outcome: "terminal", attempts, retries, reason: "cancelled" });
8581
+ if (hooks) hooks.failure = details;
8582
+ throwWithDetails(error, details);
8323
8583
  }
8324
- await cancellableBackoff(backoffMs * (attempt + 1), options?.signal);
8325
- continue;
8584
+ const classification2 = classify(facts2, recovery);
8585
+ const failure2 = await resolveFailure({
8586
+ facts: facts2,
8587
+ classification: classification2,
8588
+ attempts,
8589
+ retries,
8590
+ episode,
8591
+ hooks,
8592
+ onRetry,
8593
+ options
8594
+ });
8595
+ if (failure2.retry) {
8596
+ retries += 1;
8597
+ continue;
8598
+ }
8599
+ throwWithDetails(error, failure2.details);
8326
8600
  }
8327
- throw error;
8328
- }
8329
- if (options?.signal?.aborted) throw new Error("Subagent aborted by parent");
8330
- if (message.stopReason === "error" && attempt < 2 && isRetryableSubagentFailureFrom({ errorMessage: message.errorMessage, retryable: message.retryable, replayClass: message.replayClass })) {
8331
- try {
8332
- onRetry?.();
8333
- } catch {
8601
+ if (options?.signal?.aborted) {
8602
+ episode.resetAttempts();
8603
+ throw new Error("Subagent aborted by parent");
8334
8604
  }
8335
- await cancellableBackoff(backoffMs * (attempt + 1), options?.signal);
8336
- continue;
8337
- }
8338
- const result = new AssistantMessageEventStream();
8339
- if (message.stopReason === "error" || message.stopReason === "aborted") {
8340
- result.push({ type: "error", reason: message.stopReason, error: { ...message, content: [] } });
8341
- } else {
8342
- result.push({ type: "done", reason: message.stopReason, message });
8605
+ if (hooks?.hardDeadlineAt !== void 0 && Date.now() >= hooks.hardDeadlineAt) {
8606
+ episode.resetAttempts();
8607
+ throw new Error("Subagent hard deadline reached");
8608
+ }
8609
+ if (message.stopReason !== "error" && message.stopReason !== "aborted") {
8610
+ episode.succeed();
8611
+ if (hooks) hooks.failure = void 0;
8612
+ return singleResponse(message);
8613
+ }
8614
+ if (message.stopReason === "aborted") {
8615
+ episode.resetAttempts();
8616
+ if (hooks) hooks.failure = void 0;
8617
+ return singleResponse(message);
8618
+ }
8619
+ const facts = factsFromMessage(message);
8620
+ const classification = classify(facts, recovery);
8621
+ const failure = await resolveFailure({
8622
+ facts,
8623
+ classification,
8624
+ attempts,
8625
+ retries,
8626
+ episode,
8627
+ hooks,
8628
+ onRetry,
8629
+ options
8630
+ });
8631
+ if (failure.retry) {
8632
+ retries += 1;
8633
+ continue;
8634
+ }
8635
+ const finalMessage = failure.details.classification?.message && failure.details.classification.message !== message.errorMessage ? { ...message, errorMessage: failure.details.classification.message } : message;
8636
+ return singleResponse(finalMessage);
8343
8637
  }
8344
- result.end();
8345
- return result;
8638
+ } finally {
8639
+ episode.resetAttempts();
8346
8640
  }
8347
8641
  };
8348
8642
  }
8643
+ async function resolveFailure(params) {
8644
+ const { facts, classification, attempts, retries, episode, hooks, onRetry, options } = params;
8645
+ if (options?.signal?.aborted) {
8646
+ episode.resetAttempts();
8647
+ throw new Error("Subagent aborted by parent");
8648
+ }
8649
+ if (classification.userAbort) {
8650
+ episode.resetAttempts();
8651
+ const cancellation = {
8652
+ ...classification,
8653
+ kind: "user_abort",
8654
+ retryable: false,
8655
+ compactable: false,
8656
+ userAbort: true,
8657
+ terminal: true
8658
+ };
8659
+ const details2 = failureDetails({ ...facts, stopReason: "aborted" }, cancellation, {
8660
+ outcome: "terminal",
8661
+ attempts,
8662
+ retries,
8663
+ reason: "cancelled"
8664
+ });
8665
+ if (hooks) hooks.failure = details2;
8666
+ return { retry: false, details: details2 };
8667
+ }
8668
+ if (classification.compactable) {
8669
+ const details2 = failureDetails(facts, classification, {
8670
+ outcome: "overflow",
8671
+ attempts,
8672
+ retries,
8673
+ reason: "compaction_unavailable"
8674
+ });
8675
+ if (hooks) hooks.failure = details2;
8676
+ return { retry: false, details: details2 };
8677
+ }
8678
+ let decision = episode.next(classification);
8679
+ if (decision.action === "retry") {
8680
+ const remaining = hooks?.hardDeadlineAt === void 0 ? Infinity : hooks.hardDeadlineAt - Date.now();
8681
+ if (remaining <= 0) {
8682
+ episode.resetAttempts();
8683
+ throw new Error("Subagent hard deadline reached");
8684
+ }
8685
+ if (decision.delayMs > remaining) {
8686
+ const deadlineClassification = {
8687
+ ...classification,
8688
+ retryable: false,
8689
+ terminal: true,
8690
+ message: `${classification.message} (recovery delay ${decision.delayMs}ms exceeds the remaining subagent deadline ${Math.max(0, Math.round(remaining))}ms)`
8691
+ };
8692
+ const details2 = failureDetails(facts, deadlineClassification, {
8693
+ outcome: "exhausted",
8694
+ attempts,
8695
+ retries,
8696
+ maxRetries: decision.maxRetries,
8697
+ reason: "deadline"
8698
+ });
8699
+ if (hooks) hooks.failure = details2;
8700
+ return { retry: false, details: details2 };
8701
+ }
8702
+ try {
8703
+ onRetry?.();
8704
+ } catch {
8705
+ }
8706
+ await cancellableBackoff(decision.delayMs, options?.signal);
8707
+ if (options?.signal?.aborted) {
8708
+ episode.resetAttempts();
8709
+ throw new Error("Subagent aborted by parent");
8710
+ }
8711
+ if (hooks?.hardDeadlineAt !== void 0 && Date.now() >= hooks.hardDeadlineAt) {
8712
+ episode.resetAttempts();
8713
+ throw new Error("Subagent hard deadline reached");
8714
+ }
8715
+ return { retry: true };
8716
+ }
8717
+ const finalClassification = decision.action === "exhausted" ? decision.failure : classification;
8718
+ const details = failureDetails(facts, finalClassification, {
8719
+ outcome: decision.action === "exhausted" ? "exhausted" : "terminal",
8720
+ attempts,
8721
+ retries,
8722
+ ...decision.action !== "terminal" ? { maxRetries: decision.maxRetries } : {},
8723
+ ...decision.action === "exhausted" ? { reason: decision.reason } : {}
8724
+ });
8725
+ if (hooks) hooks.failure = details;
8726
+ return { retry: false, details };
8727
+ }
8349
8728
 
8350
8729
  // ../../packages/shared-headless-capabilities/src/subagent-sessions.ts
8351
8730
  var SUBAGENT_SESSION_RECORD_VERSION = 1;
@@ -8525,14 +8904,14 @@ function lastPipelineCommand(command) {
8525
8904
  const segments = command.split("|").map((part) => part.trim()).filter(Boolean);
8526
8905
  return segments[segments.length - 1] || command;
8527
8906
  }
8528
- function splitShellSegments(command) {
8907
+ function splitShellSegments(command, shell = "bash") {
8529
8908
  const segments = [];
8530
8909
  let current = "";
8531
8910
  let quote;
8532
8911
  for (let index = 0; index < command.length; index += 1) {
8533
8912
  const char = command[index];
8534
8913
  if (quote) {
8535
- if (quote === '"' && char === "\\" && index + 1 < command.length) {
8914
+ if (quote === '"' && char === (shell === "powershell" ? "`" : "\\") && index + 1 < command.length) {
8536
8915
  current += char;
8537
8916
  current += command[index + 1];
8538
8917
  index += 1;
@@ -8547,13 +8926,13 @@ function splitShellSegments(command) {
8547
8926
  current += char;
8548
8927
  continue;
8549
8928
  }
8550
- if (char === "\\" && index + 1 < command.length) {
8929
+ if (char === (shell === "powershell" ? "`" : "\\") && index + 1 < command.length) {
8551
8930
  current += char;
8552
8931
  current += command[index + 1];
8553
8932
  index += 1;
8554
8933
  continue;
8555
8934
  }
8556
- if (char === "&" || char === "|" || char === ";" || char === "\n") {
8935
+ if (char === "&" || char === "|" || char === ";" || char === "\n" || shell === "powershell" && /[(){}]/.test(char)) {
8557
8936
  if ((char === "&" || char === "|") && command[index + 1] === char) {
8558
8937
  index += 1;
8559
8938
  }
@@ -8566,7 +8945,7 @@ function splitShellSegments(command) {
8566
8945
  segments.push(current);
8567
8946
  return segments.map((part) => part.trim()).filter(Boolean);
8568
8947
  }
8569
- function splitShellWords(segment) {
8948
+ function splitShellWords(segment, shell = "bash") {
8570
8949
  const words = [];
8571
8950
  let current = "";
8572
8951
  let quote;
@@ -8575,7 +8954,7 @@ function splitShellWords(segment) {
8575
8954
  if (quote) {
8576
8955
  if (char === quote) {
8577
8956
  quote = void 0;
8578
- } else if (quote === '"' && char === "\\" && index + 1 < segment.length) {
8957
+ } else if (quote === '"' && char === (shell === "powershell" ? "`" : "\\") && index + 1 < segment.length) {
8579
8958
  current += segment[index + 1];
8580
8959
  index += 1;
8581
8960
  } else {
@@ -8587,7 +8966,7 @@ function splitShellWords(segment) {
8587
8966
  quote = char;
8588
8967
  continue;
8589
8968
  }
8590
- if (char === "\\") {
8969
+ if (char === (shell === "powershell" ? "`" : "\\")) {
8591
8970
  if (index + 1 >= segment.length) return void 0;
8592
8971
  current += segment[index + 1];
8593
8972
  index += 1;
@@ -8796,7 +9175,7 @@ function classifyShellCommand(command, shell = "bash", options) {
8796
9175
  const warnings = [];
8797
9176
  let commandClass = "unknown_shell";
8798
9177
  let destructive = false;
8799
- const destructivePatterns = shell === "powershell" ? [...POWERSHELL_DESTRUCTIVE_PATTERNS, ...DESTRUCTIVE_PATTERNS] : shell === "cmd" ? [...CMD_DESTRUCTIVE_PATTERNS, ...DESTRUCTIVE_PATTERNS] : DESTRUCTIVE_PATTERNS;
9178
+ const destructivePatterns = shell === "cmd" ? [...CMD_DESTRUCTIVE_PATTERNS, ...DESTRUCTIVE_PATTERNS] : DESTRUCTIVE_PATTERNS;
8800
9179
  const removeWarnings = recursiveRemoveWarnings(command);
8801
9180
  if (removeWarnings.length > 0) {
8802
9181
  if (shell === "bash" && isStaticallyScopedRecursiveForceRemove(command, options)) {
@@ -8813,6 +9192,37 @@ function classifyShellCommand(command, shell = "bash", options) {
8813
9192
  commandClass = "unknown_shell";
8814
9193
  destructive = true;
8815
9194
  }
9195
+ if (shell === "powershell") {
9196
+ const segments = splitShellSegments(command, shell);
9197
+ const simple = !/[$`#@{}()[\]=<>]/.test(command) && segments.every((segment) => {
9198
+ const words2 = splitShellWords(segment, shell);
9199
+ if (!words2?.length) return false;
9200
+ const head = words2[0] === "." ? words2[1] : words2[0];
9201
+ return !!head && /^[A-Za-z0-9_.\\/][A-Za-z0-9_.\\/:-]*$/.test(head) && !/^(?:return|throw|exit|if|elseif|else|switch|foreach|for|while|do|until|try|catch|finally|function|filter|class|begin|process|end|trap|data|using|workflow|configuration|break|continue|inlinescript|parallel|sequence)$/i.test(head);
9202
+ });
9203
+ if (!simple) {
9204
+ for (const entry of POWERSHELL_DESTRUCTIVE_PATTERNS) {
9205
+ if (!entry.pattern.test(command)) continue;
9206
+ warnings.push(entry.warning);
9207
+ commandClass = entry.commandClass;
9208
+ destructive = true;
9209
+ }
9210
+ }
9211
+ for (const segment of segments) {
9212
+ const words2 = splitShellWords(segment, shell) ?? segment.split(/\s+/);
9213
+ if (words2[0] === ".") words2.shift();
9214
+ if (!words2[0]) continue;
9215
+ words2[0] = executableName(words2[0]);
9216
+ for (const entry of POWERSHELL_DESTRUCTIVE_PATTERNS) {
9217
+ const pattern = new RegExp(`^(?:${entry.pattern.source})`, entry.pattern.flags);
9218
+ if (pattern.test(words2.join(" "))) {
9219
+ warnings.push(entry.warning);
9220
+ commandClass = entry.commandClass;
9221
+ destructive = true;
9222
+ }
9223
+ }
9224
+ }
9225
+ }
8816
9226
  for (const entry of destructivePatterns) {
8817
9227
  if (entry.pattern.test(command)) {
8818
9228
  warnings.push(entry.warning);
@@ -8828,8 +9238,6 @@ function classifyShellCommand(command, shell = "bash", options) {
8828
9238
  const primary = words[0] || firstWord(normalized);
8829
9239
  if (shell === "powershell" && /\b(invoke-webrequest|iwr|invoke-restmethod|irm)\b/.test(normalized)) {
8830
9240
  commandClass = "network";
8831
- } else if (shell === "powershell" && /\b(new-item|set-content|add-content|copy-item|move-item|rename-item)\b/.test(normalized)) {
8832
- commandClass = "filesystem_write";
8833
9241
  } else if (shell === "cmd" && /\b(curl|bitsadmin|certutil)\b/.test(normalized)) {
8834
9242
  commandClass = "network";
8835
9243
  } else if (shell === "cmd" && /\b(copy|xcopy|robocopy|move|mkdir|md|ren|rename)\b/.test(normalized)) {
@@ -9131,7 +9539,7 @@ function createAgentInstance(deps, systemPrompt, tools, model, apiKey, thinkingL
9131
9539
  hooks.observation.lastWorkAt = Date.now();
9132
9540
  hooks.observation.lastActivity = "response_event";
9133
9541
  }
9134
- }, hooks);
9542
+ }, hooks, deps.recovery);
9135
9543
  if (deps.createAgent) {
9136
9544
  const agent2 = deps.createAgent({
9137
9545
  streamFn,
@@ -9160,6 +9568,22 @@ function createAgentInstance(deps, systemPrompt, tools, model, apiKey, thinkingL
9160
9568
  agentRunHooks.set(agent, hooks);
9161
9569
  return agent;
9162
9570
  }
9571
+ function inFlightRequestSummary(observation, now) {
9572
+ const transport = observation?.transport;
9573
+ if (!transport || transport.length === 0) return void 0;
9574
+ const lastStartIndex = transport.map((event) => event.phase).lastIndexOf("request_start");
9575
+ if (lastStartIndex < 0) return void 0;
9576
+ const start = transport[lastStartIndex];
9577
+ const after = transport.slice(lastStartIndex + 1);
9578
+ const terminal = after.find((event) => event.phase === "terminal_accepted");
9579
+ if (terminal) {
9580
+ const gapMs = Math.max(0, now - terminal.at);
9581
+ return `last model response completed ${Math.round(gapMs / 100) / 10}s before the deadline; no request in flight at abort`;
9582
+ }
9583
+ const sawFirst = after.some((event) => event.phase === "first_application_message");
9584
+ const ageMs = Math.max(0, now - start.at);
9585
+ return `a model request was still in flight at abort (${Math.round(ageMs / 100) / 10}s old, ${sawFirst ? "receiving stream events" : "no application message yet"}) \u2014 the deadline cut an unfinished response, which usually means the budget is too small for this reasoning level, not a transport stall`;
9586
+ }
9163
9587
  function isParentHandoffRequired(task) {
9164
9588
  return task.report?.availability !== "available" || task.mode === "edit" || task.status === "timeout" || task.status === "failed" || task.confidenceSignals.some((signal) => signal.type === "tool_error" || signal.type === "no_tool_use" || signal.type === "no_text_report");
9165
9589
  }
@@ -9605,10 +10029,21 @@ ${DELEGATION_REPORT_INSTRUCTION}`;
9605
10029
  throw new Error(`Subagent output token limit reached (stopReason=length): the provider cut the final response before the report was produced${messages.some(messageHasOwnText) ? " (earlier-turn narration discarded)" : ", no text response"}. The output/thinking budget was too small for this task at its reasoning level. Rerun with a narrower task or a lower thinkingLevel.`);
9606
10030
  }
9607
10031
  const detail = terminalError ? `: ${terminalError.slice(0, 200)}` : "";
9608
- const failure = new Error(`Subagent response failed (provider/transport failure before the report was produced, stopReason=${terminalStopReason ?? "none"}${collectedResponse ? ", partial transcript discarded" : ", no text response"})${detail}`);
10032
+ const hooks2 = agentRunHooks.get(subagent);
10033
+ const fallbackFailure = lastAssistant && (lastAssistant.retryable !== void 0 || lastAssistant.replayClass !== void 0 || lastAssistant.replayEligible !== void 0 || lastAssistant.remoteFailure !== void 0) ? {
10034
+ errorMessage: terminalError,
10035
+ retryable: lastAssistant.retryable,
10036
+ replayClass: lastAssistant.replayClass,
10037
+ replayEligible: lastAssistant.replayEligible,
10038
+ remoteFailure: lastAssistant.remoteFailure
10039
+ } : void 0;
10040
+ const subagentFailure = hooks2?.failure ?? fallbackFailure;
10041
+ const overflow = subagentFailure?.recovery?.outcome === "overflow";
10042
+ const failure = new Error(overflow ? `Subagent context overflow requires compaction, but no subagent compactor is configured${detail}` : `Subagent response failed (provider/transport failure before the report was produced, stopReason=${terminalStopReason ?? "none"}${collectedResponse ? ", partial transcript discarded" : ", no text response"})${detail}`);
9609
10043
  const carrier = failure;
9610
- carrier.retryable = lastAssistant?.retryable;
9611
- carrier.replayClass = lastAssistant?.replayClass;
10044
+ carrier.retryable = subagentFailure?.retryable;
10045
+ carrier.replayClass = subagentFailure?.replayClass;
10046
+ carrier.subagentFailure = subagentFailure;
9612
10047
  throw failure;
9613
10048
  }
9614
10049
  let response;
@@ -9653,17 +10088,25 @@ ${DELEGATION_REPORT_INSTRUCTION}`;
9653
10088
  task.status = timedOut ? "timeout" : signal?.aborted ? "cancelled" : "failed";
9654
10089
  observeDelegationPhase(task.observation, timedOut ? "deadline" : signal?.aborted ? "cancelled" : "failed");
9655
10090
  phaseProgress(task.observation.phase);
9656
- task.error = errorMessage.slice(0, 2e3);
10091
+ const deadlineDetail = timedOut ? inFlightRequestSummary(task.observation, Date.now()) : void 0;
10092
+ task.error = `${errorMessage.slice(0, 2e3)}${deadlineDetail ? `; ${deadlineDetail}` : ""}`.slice(0, 2e3);
9657
10093
  const failureCarrier = error;
9658
- if (failureCarrier && (failureCarrier.retryable !== void 0 || failureCarrier.replayClass !== void 0)) {
9659
- task.failure = { ...failureCarrier.retryable !== void 0 ? { retryable: failureCarrier.retryable } : {}, ...failureCarrier.replayClass !== void 0 ? { replayClass: failureCarrier.replayClass } : {} };
10094
+ if (failureCarrier?.subagentFailure) {
10095
+ task.failure = failureCarrier.subagentFailure;
10096
+ } else if (failureCarrier && (failureCarrier.retryable !== void 0 || failureCarrier.replayClass !== void 0 || failureCarrier.replayEligible !== void 0 || failureCarrier.remoteFailure !== void 0)) {
10097
+ task.failure = {
10098
+ ...failureCarrier.retryable !== void 0 ? { retryable: failureCarrier.retryable } : {},
10099
+ ...failureCarrier.replayClass !== void 0 ? { replayClass: failureCarrier.replayClass } : {},
10100
+ ...failureCarrier.replayEligible !== void 0 ? { replayEligible: failureCarrier.replayEligible } : {},
10101
+ ...failureCarrier.remoteFailure !== void 0 ? { remoteFailure: failureCarrier.remoteFailure } : {}
10102
+ };
9660
10103
  } else {
9661
10104
  task.failure = void 0;
9662
10105
  }
9663
10106
  task.report ??= parseDelegationReport(void 0, task.id);
9664
10107
  task.parentHandoffRequired = isParentHandoffRequired(task);
9665
10108
  task.completedAt = Date.now();
9666
- report({ message: `[${task.name}] Failed: ${errorMessage.slice(0, 100)}`, increment: 100 });
10109
+ report({ message: `[${task.name}] Failed: ${(errorMessage + (deadlineDetail ? `; ${deadlineDetail}` : "")).slice(0, 160)}`, increment: 100 });
9667
10110
  } finally {
9668
10111
  if (heartbeat) {
9669
10112
  clearInterval(heartbeat);
@@ -9741,6 +10184,11 @@ function buildParallelResult(tasks, totalDuration) {
9741
10184
  `;
9742
10185
  if (task.observation) resultText += `Tools: ${task.observation.toolStarts} started, ${task.observation.toolEnds} ended, ${task.observation.toolErrors} errors
9743
10186
  `;
10187
+ if (task.failure?.recovery) {
10188
+ const recovery = task.failure.recovery;
10189
+ resultText += `Recovery: ${task.failure.classification?.kind ?? "unclassified"}; outcome=${recovery.outcome}${recovery.reason ? ` (${recovery.reason})` : ""}; attempts=${recovery.attempts}; retries=${recovery.retries}${recovery.maxRetries !== void 0 ? `; budget=${recovery.maxRetries}` : ""}
10190
+ `;
10191
+ }
9744
10192
  if (task.status === "completed" && task.result) {
9745
10193
  resultText += `${task.result}
9746
10194
  `;
@@ -9753,7 +10201,7 @@ function buildParallelResult(tasks, totalDuration) {
9753
10201
  `;
9754
10202
  }
9755
10203
  if (task.sessionId) {
9756
- resultText += task.sessionBusy ? `Session: ${task.sessionId} (busy \u2014 previous run is still settling; recall will fail until it settles` : `Session: ${task.sessionId} (resumable \u2014 pass as "sessionId" in a later spawn to continue this subagent with its accumulated context`;
10204
+ resultText += task.sessionBusy ? `Session: ${task.sessionId} (busy \u2014 previous run is still settling; recall will fail until it settles` : `Session: ${task.sessionId} (resumable \u2014 pass as "sessionId" in a later spawn to continue this subagent with its accumulated context; add resumeMode:"report" to reuse only its last report`;
9757
10205
  if (task.sessionRunCount) resultText += `; run ${task.sessionRunCount}`;
9758
10206
  if (task.sessionPersisted) resultText += "; persisted to disk, survives restarts";
9759
10207
  resultText += ")\n";
@@ -9929,7 +10377,7 @@ Default mode="read" is for investigation, search, review, or triage and cannot e
9929
10377
  priority: Type.Optional(Type.Union([Type.Literal("fast"), Type.Literal("thorough")], { description: "fast uses lower-cost defaults; thorough raises model class when needed." })),
9930
10378
  model: Type.Optional(Type.String({ description: 'Explicit model override, e.g. "provider/model-id". Only when the user names a specific model; otherwise automatic.' })),
9931
10379
  thinkingLevel: Type.Optional(Type.Union([Type.Literal("off"), Type.Literal("minimal"), Type.Literal("low"), Type.Literal("medium"), Type.Literal("high"), Type.Literal("xhigh")], { description: "off|minimal|low|medium|high|xhigh. Default medium; override only when the user asks or for cheap lookups (off)." })),
9932
- timeout: Type.Optional(Type.Number({ description: "Seconds. Default 1200; set higher for long tasks." })),
10380
+ timeout: Type.Optional(Type.Number({ description: "Seconds. Default 1200; set higher for long tasks. Note: high/xhigh reasoning tasks can spend minutes per response on thinking alone \u2014 budget accordingly or the deadline will cut an in-flight response mid-thought." })),
9933
10381
  resumeMode: Type.Optional(Type.Union([Type.Literal("full"), Type.Literal("report")], { description: "Default full retains prior transcript. Explicit report rebuilds with current tools/model/credentials and only the latest valid historical report as data plus the new task; fails if unavailable." })),
9934
10382
  sessionId: Type.Optional(Type.String({ description: "Continue a previous subagent session by id (from an earlier result's Session line); full mode retains historical input with unknown cost. Optional mode/model/thinkingLevel reconfigure it; one task at a time; recalls never replay on failure and must target an idle session; prefer a fresh task for independent reviews." }))
9935
10383
  }
@@ -9992,40 +10440,8 @@ ${SPAWN_USAGE_HINT}` }],
9992
10440
  toolContext.log?.(`Spawning ${tasks.length} parallel subagents: ${tasks.map((task) => task.name).join(", ")}`);
9993
10441
  toolContext.reportProgress?.({ message: `Launching ${tasks.length} parallel subagents...`, increment: 5 });
9994
10442
  const startTime = Date.now();
9995
- const runTask = async (task) => {
9996
- await run(task, toolContext.reportProgress, signal);
9997
- if (task.wasRecall || task.mode !== "read" || task.status !== "failed" || !task.error) return;
9998
- if (signal?.aborted) return;
9999
- if (!isRetryableSubagentFailureFrom({ errorMessage: task.error, retryable: task.failure?.retryable, replayClass: task.failure?.replayClass })) return;
10000
- observeDelegationPhase(task.observation, "task_replay");
10001
- const settleMs = Math.max(0, (deps.transientRetryBackoffMs ?? 2e3) * 10);
10002
- toolContext.reportProgress?.({ message: `[${task.name}] Provider/transport failure \u2014 replaying the read task once after ${Math.round(settleMs / 1e3)}s`, increment: 0 });
10003
- await cancellableBackoff(settleMs, signal);
10004
- if (signal.aborted) {
10005
- task.status = "cancelled";
10006
- task.error = "Subagent cancelled during replay backoff";
10007
- task.failure = void 0;
10008
- observeDelegationPhase(task.observation, "cancelled");
10009
- task.completedAt = Date.now();
10010
- return;
10011
- }
10012
- task.evidence = [];
10013
- task.confidenceSignals = [];
10014
- task.runtimeEvidence = [];
10015
- task.reportedEvidence = [];
10016
- task.error = void 0;
10017
- task.failure = void 0;
10018
- task.report = void 0;
10019
- task.result = void 0;
10020
- task.details = void 0;
10021
- task.parentHandoffRequired = false;
10022
- task.sessionId = void 0;
10023
- task.requestedMode = void 0;
10024
- task.wasRecall = void 0;
10025
- await run(task, toolContext.reportProgress, signal);
10026
- };
10027
10443
  const batch = Promise.resolve().then(() => runWithConcurrencyLimit(
10028
- tasks.map((task) => () => runTask(task)),
10444
+ tasks.map((task) => () => run(task, toolContext.reportProgress, signal)),
10029
10445
  Math.max(1, deps.maxConcurrentSubagents ?? DEFAULT_MAX_CONCURRENT_SUBAGENTS)
10030
10446
  ));
10031
10447
  pending.add(batch);
@@ -10068,9 +10484,8 @@ ${SPAWN_USAGE_HINT}` }],
10068
10484
  observation: task.observation,
10069
10485
  parentHandoffRequired: task.parentHandoffRequired,
10070
10486
  failureReason: task.error,
10071
- // Structured provider verdict so the parent model can distinguish
10072
- // stream loss (safe to re-dispatch; no tool calls executed) from hard
10073
- // provider vetoes (auth/quota/config — not recoverable here).
10487
+ // Structured provider facts plus the actual recovery outcome. Failed-
10488
+ // response tool calls never execute; do not infer their absence.
10074
10489
  failure: task.failure,
10075
10490
  details: task.details
10076
10491
  })),
@@ -10293,7 +10708,11 @@ function executeManageTodoList(args, currentTodos) {
10293
10708
  };
10294
10709
  }
10295
10710
  default:
10296
- return rejectTodoAction("read", `unknown action: ${args.action}`, safeCurrentTodos);
10711
+ return rejectTodoAction(
10712
+ "read",
10713
+ `unknown action: ${args.action}. Valid actions: create, read, complete_current, clear.`,
10714
+ safeCurrentTodos
10715
+ );
10297
10716
  }
10298
10717
  }
10299
10718
 
@@ -11142,6 +11561,7 @@ var resolveToCwd = resolveInSandbox;
11142
11561
  var resolveReadPath = resolveInSandbox;
11143
11562
 
11144
11563
  // ../../packages/shared-headless-capabilities/src/builtin/fs/schema-utils.ts
11564
+ var SCOPE_BASELINE_SENTENCE = "Absolute paths outside the sandbox are accepted only within configured access roots or when the current runtime mode's allowlist permits them; the effective scope is stated in the runtime-mode context, not in this description.";
11145
11565
  function formatPrefixes(prefixes) {
11146
11566
  if (!prefixes || prefixes.length === 0) {
11147
11567
  return "";
@@ -11151,6 +11571,11 @@ function formatPrefixes(prefixes) {
11151
11571
  return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
11152
11572
  }).join(", ");
11153
11573
  }
11574
+ function arraysEqual(a, b) {
11575
+ if (a === b) return true;
11576
+ if (a.length !== b.length) return false;
11577
+ return a.every((value, index) => value === b[index]);
11578
+ }
11154
11579
  function buildRootParameterSchema(sandboxRoot, options) {
11155
11580
  const roots = getAvailableRoots(options);
11156
11581
  if (roots.length === 0) {
@@ -11174,8 +11599,11 @@ function buildRootParameterSchema(sandboxRoot, options) {
11174
11599
  function buildPathResolutionDescription(sandboxRoot, options) {
11175
11600
  const roots = getAvailableRoots(options);
11176
11601
  const allowlistedDirs = options?.allowlistedDirs ?? [];
11177
- const hasGlobalAllowlist = allowlistedDirs.some((dir) => dir.trim() === "/");
11178
- const allowlistSentence = hasGlobalAllowlist ? "Absolute paths anywhere on the local filesystem are accepted in the current runtime mode." : allowlistedDirs.length > 0 ? `Absolute paths inside allowlisted directories are accepted: ${allowlistedDirs.join(", ")}.` : "";
11602
+ const descriptionOverride = options?.descriptionAllowlistedDirs;
11603
+ const describedAllowlist = descriptionOverride ?? allowlistedDirs;
11604
+ const viewsEqual = arraysEqual(describedAllowlist, allowlistedDirs);
11605
+ const hasGlobalAllowlist = describedAllowlist.some((dir) => dir.trim() === "/") && viewsEqual;
11606
+ const allowlistSentence = hasGlobalAllowlist ? "Absolute paths anywhere on the local filesystem are accepted in the current runtime mode." : viewsEqual ? describedAllowlist.length > 0 ? `Absolute paths inside allowlisted directories are accepted: ${describedAllowlist.join(", ")}.` : "" : SCOPE_BASELINE_SENTENCE;
11179
11607
  if (roots.length === 0) {
11180
11608
  return ["Relative paths resolve against the workspace root.", allowlistSentence].filter(Boolean).join(" ");
11181
11609
  }
@@ -11700,9 +12128,10 @@ Focus: ${question.trim()}` : ""}` },
11700
12128
  return;
11701
12129
  }
11702
12130
  if (magicInfo?.kind === "image") {
12131
+ const inlineSupported = INLINE_IMAGE_MIME_TYPES.has(magicInfo.mimeType);
11703
12132
  resolve2({
11704
- content: [{ type: "text", text: `Cannot attach image file ${path3}: unsupported image payload ${magicInfo.mimeType}.` }],
11705
- details: { understanding: { kind: "image", mimeType: magicInfo.mimeType, fallbackReason: "unsupported inline image format" } },
12133
+ content: [{ type: "text", text: inlineSupported ? `Cannot attach image file ${path3}: the host filesystem bridge returned no usable image data for ${magicInfo.mimeType} (see logs for readImageInfo diagnostics).` : `Cannot attach image file ${path3}: unsupported image payload ${magicInfo.mimeType}.` }],
12134
+ details: { understanding: { kind: "image", mimeType: magicInfo.mimeType, fallbackReason: inlineSupported ? "image bridge read failed" : "unsupported inline image format" } },
11706
12135
  isError: true
11707
12136
  });
11708
12137
  return;
@@ -12109,11 +12538,10 @@ function detectTextOnlyBinary(input, filePath, prefixMayBeTruncated) {
12109
12538
  async function readImageInfo(path3, fs2) {
12110
12539
  try {
12111
12540
  let base64 = "";
12112
- const size = (await fs2.stat(path3).catch(() => void 0))?.size ?? 0;
12113
12541
  if (typeof fs2.readFile === "function") {
12114
12542
  base64 = await fs2.readFile(path3, "base64").catch(() => "");
12115
12543
  }
12116
- if (!base64 && size > 0 && typeof fs2.readFileSync === "function") {
12544
+ if (!base64 && typeof fs2.readFileSync === "function") {
12117
12545
  base64 = fs2.readFileSync(path3, "base64");
12118
12546
  }
12119
12547
  if (!base64) {
@@ -12141,6 +12569,8 @@ async function readImageInfo(path3, fs2) {
12141
12569
  if (bytes.length >= 12 && asciiAt(bytes, 0, 4) === "RIFF" && asciiAt(bytes, 8, 12 - 8) === "WEBP") {
12142
12570
  return { mimeType: "image/webp", base64, ...parseWebpDimensions(bytes) };
12143
12571
  }
12572
+ console.error(`[pie] readImageInfo decoded ${bytes.length} bytes for ${path3} but no known image signature matched`);
12573
+ return void 0;
12144
12574
  } catch (error) {
12145
12575
  console.error(`[pie] readImageInfo failed for ${path3}: ${error instanceof Error ? error.message : String(error)}`);
12146
12576
  return void 0;
@@ -12148,7 +12578,7 @@ async function readImageInfo(path3, fs2) {
12148
12578
  return void 0;
12149
12579
  }
12150
12580
  function decodeBase64ToBytes(base64) {
12151
- if (typeof Buffer !== "undefined") {
12581
+ if (hasUsableNodeBuffer()) {
12152
12582
  return Buffer.from(base64, "base64");
12153
12583
  }
12154
12584
  const clean = base64.replace(/[^A-Za-z0-9+/=]/g, "");
@@ -12167,6 +12597,17 @@ function decodeBase64ToBytes(base64) {
12167
12597
  }
12168
12598
  return bytes.subarray(0, at);
12169
12599
  }
12600
+ function hasUsableNodeBuffer() {
12601
+ if (typeof Buffer === "undefined" || typeof Buffer.from !== "function") {
12602
+ return false;
12603
+ }
12604
+ try {
12605
+ const probe = Buffer.from("aGk=", "base64");
12606
+ return probe.length === 2 && probe[0] === 104;
12607
+ } catch {
12608
+ return false;
12609
+ }
12610
+ }
12170
12611
  function readUIntBE(bytes, offset, length) {
12171
12612
  let value = 0;
12172
12613
  for (let i = 0; i < length; i += 1) {