@cydm/pie 2.7.10 → 2.7.12

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.
@@ -2,7 +2,7 @@ import { createRequire as __createRequire } from "node:module"; const require =
2
2
  import {
3
3
  Agent,
4
4
  getToolCallerIdentity
5
- } from "./chunk-JO6EDWHP.js";
5
+ } from "./chunk-2NBUFVUQ.js";
6
6
  import {
7
7
  AssistantMessageEventStream,
8
8
  FileSystemGateway,
@@ -12,7 +12,7 @@ import {
12
12
  getPlatformConfig,
13
13
  hashText,
14
14
  streamSimple
15
- } from "./chunk-2B6VS2NH.js";
15
+ } from "./chunk-EN47TIBG.js";
16
16
  import {
17
17
  Type
18
18
  } from "./chunk-XXGYUYXI.js";
@@ -1003,9 +1003,9 @@ var PLAN_POWERSHELL_READ_COMMANDS = new Set([
1003
1003
  ].map((name) => name.toLowerCase()));
1004
1004
  var PLAN_POWERSHELL_READ_ALIASES = /* @__PURE__ */ new Set(["cat", "ls", "pwd", "echo", "sort"]);
1005
1005
  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}|\||&&|\|\|/;
1006
+ var PLAN_POWERSHELL_SEPARATORS = /[;\n\r\t&<>|]/;
1007
1007
  var PLAN_POWERSHELL_NON_ASCII = /[^\x20-\x7E]/;
1008
- var PLAN_POWERSHELL_UNSAFE_TOKENS = /\$\(|`|%\{|[(){}\[\]]|\bsource\b|\.\s+[/~]|\b(bash|sh|zsh|pwsh|powershell|cmd|npm|npx|node|python3?|git)\b/i;
1008
+ var PLAN_POWERSHELL_UNSAFE_TOKENS = /\$\(|`|%\{|[(){}\[\]]|\bsource\b|\.\s+[/~]|\b(bash|sh|zsh|pwsh|powershell|cmd|npm|npx|node|python3?)\b/i;
1009
1009
  function isPlanModeSafePowerShellCommand(command) {
1010
1010
  const trimmed = command.trim();
1011
1011
  if (!trimmed) return false;
@@ -1015,6 +1015,18 @@ function isPlanModeSafePowerShellCommand(command) {
1015
1015
  const tokens = trimmed.split(/\s+/).filter(Boolean);
1016
1016
  const head = tokens[0].toLowerCase();
1017
1017
  if (!head) return false;
1018
+ if (head === "git" || head === "rg") {
1019
+ if (/["']/.test(trimmed)) return false;
1020
+ if (/[$@]/.test(trimmed)) return false;
1021
+ }
1022
+ if (head === "git") {
1023
+ const subcommand = tokens[1]?.toLowerCase() ?? "";
1024
+ if (!GIT_READ_SUBCOMMANDS.has(subcommand)) return false;
1025
+ return !tokens.slice(2).some((token) => UNSAFE_GIT_FLAGS.test(token) || token === "--no-pager" || token === "-c");
1026
+ }
1027
+ if (head === "rg") {
1028
+ return !tokens.slice(1).some((token) => UNSAFE_RG_FLAGS.test(token) || token === "--pre" || token === "--pre-glob");
1029
+ }
1018
1030
  if (!PLAN_POWERSHELL_READ_COMMANDS.has(head) && !PLAN_POWERSHELL_READ_ALIASES.has(head)) return false;
1019
1031
  for (const token of tokens.slice(1)) {
1020
1032
  if (token.startsWith("-") && !PLAN_POWERSHELL_SAFE_PARAMETERS.test(token)) return false;
@@ -1319,7 +1331,9 @@ var DEFAULT_COMPACTION_CONFIG = {
1319
1331
  summaryPrefix: SUMMARY_PREFIX
1320
1332
  };
1321
1333
  var APPROX_BYTES_PER_TOKEN = 4;
1322
- var RESIZED_IMAGE_BYTES_ESTIMATE = 7373;
1334
+ var IMAGE_MEDIA_BYTES_ESTIMATE = 7373;
1335
+ var AUDIO_MEDIA_BYTES_ESTIMATE = 6800;
1336
+ var VIDEO_MEDIA_BYTES_ESTIMATE = 16e3;
1323
1337
  function estimateTokens(message) {
1324
1338
  return approximateBytesAsTokens(estimateModelVisibleBytes(message));
1325
1339
  }
@@ -1444,20 +1458,16 @@ function planRetainedTail(summaries, options = {}) {
1444
1458
  if (boundaries.length === 0) {
1445
1459
  return nonePlan("no-boundary");
1446
1460
  }
1447
- const prefix = new Array(summaries.length + 1).fill(0);
1448
- for (let i = 0; i < summaries.length; i++) {
1449
- prefix[i + 1] = prefix[i] + Math.max(0, Math.floor(summaries[i].estTokens || 0));
1450
- }
1451
1461
  const groupEnd = boundaries.map((_, index) => boundaries[index + 1] ?? summaries.length);
1452
1462
  const planFrom = (startIndex, reason) => ({
1453
1463
  retainedTail: { kind: "from-entry", entryId: summaries[startIndex].id },
1454
1464
  entryIds: summaries.slice(startIndex).map((summary) => summary.id),
1455
- tailTokens: prefix[summaries.length] - prefix[startIndex],
1465
+ tailTokens: estimateRetainedTailTokens(summaries, { kind: "from-entry", entryId: summaries[startIndex].id }),
1456
1466
  reason
1457
1467
  });
1458
1468
  let accumulated = 0;
1459
1469
  for (let k = boundaries.length - 1; k >= 0; k--) {
1460
- const groupTokens = prefix[groupEnd[k]] - prefix[boundaries[k]];
1470
+ const groupTokens = summaries.slice(boundaries[k], groupEnd[k]).reduce((sum, summary) => sum + Math.max(0, Math.floor(summary.estTokens || 0)), 0);
1461
1471
  if (groupTokens > budget) {
1462
1472
  if (accumulated === 0) {
1463
1473
  return nonePlan("single-group-over-budget");
@@ -1533,6 +1543,9 @@ function estimateCompactionGain(params) {
1533
1543
  const tokensBefore = Math.max(0, params.tokensBefore);
1534
1544
  const tokenSavings = Math.max(0, tokensBefore - projectedTokens);
1535
1545
  const estimate = { summaryTokens, tailTokens, projectedTokens, tokenSavings };
1546
+ if (!Number.isFinite(projectedTokens)) {
1547
+ return { ...estimate, ok: false, reason: "retained context has unknown model-visible size; supply a compaction modelView" };
1548
+ }
1536
1549
  if (params.budgetTokens !== void 0 && params.budgetTokens > 0 && projectedTokens > params.budgetTokens) {
1537
1550
  return {
1538
1551
  ...estimate,
@@ -1813,7 +1826,7 @@ function normalizeUserContentBlock(block) {
1813
1826
  return { type: "text", text: block.text };
1814
1827
  }
1815
1828
  if (block.type === "hostContext") {
1816
- return { type: "text", text: "x".repeat(512) };
1829
+ return { type: "hostContext" };
1817
1830
  }
1818
1831
  if (block.type === "image") {
1819
1832
  return { type: "image", mimeType: block.mimeType };
@@ -1845,22 +1858,16 @@ function estimateMessageMediaBytes(message) {
1845
1858
  }
1846
1859
  function estimateUserContentBlockMediaBytes(block) {
1847
1860
  if (block.type === "image") {
1848
- return estimateImageContentBytes(block);
1861
+ return IMAGE_MEDIA_BYTES_ESTIMATE;
1849
1862
  }
1850
1863
  if (block.type === "audio") {
1851
- return estimateInlineDataBytes(block);
1864
+ return AUDIO_MEDIA_BYTES_ESTIMATE;
1852
1865
  }
1853
1866
  if (block.type === "video") {
1854
- return estimateInlineDataBytes(block);
1867
+ return VIDEO_MEDIA_BYTES_ESTIMATE;
1855
1868
  }
1856
1869
  return 0;
1857
1870
  }
1858
- function estimateImageContentBytes(block) {
1859
- return Math.max(RESIZED_IMAGE_BYTES_ESTIMATE, estimateInlineDataBytes(block));
1860
- }
1861
- function estimateInlineDataBytes(block) {
1862
- return utf8ByteLength(block.data || "");
1863
- }
1864
1871
  function approximateBytesAsTokens(bytes) {
1865
1872
  return Math.ceil(bytes / APPROX_BYTES_PER_TOKEN);
1866
1873
  }
@@ -4611,10 +4618,26 @@ function prepareSessionCompaction(params) {
4611
4618
  const agentOnlyFailure = excludedFailure === null && requireExclusion ? agentTail ?? null : null;
4612
4619
  const summaries = sessionManager.getActiveContextSummaries();
4613
4620
  const entries = sessionManager.getResidentContextEntries();
4614
- const effectiveSummaries = excludedFailure ? summaries.filter((summary) => summary.id !== excludedFailure.entryId) : summaries;
4621
+ const sourceSummaries = excludedFailure ? summaries.filter((summary) => summary.id !== excludedFailure.entryId) : summaries;
4615
4622
  const effectiveEntries = excludedFailure ? entries.filter((entry) => entry.id !== excludedFailure.entryId) : entries;
4616
4623
  const summaryInput = excludedFailure || agentOnlyFailure ? messages.slice(0, -1) : messages;
4617
- const inputTokens = calculateTotalTokens(summaryInput);
4624
+ const estimates = /* @__PURE__ */ new Map();
4625
+ const estimate = (message) => {
4626
+ const cached = estimates.get(message);
4627
+ if (cached !== void 0) return cached;
4628
+ const visible = params.modelView ? params.modelView(message) : message;
4629
+ const unknown = visible.role === "user" && Array.isArray(visible.content) && visible.content.some((block) => block.type === "hostContext");
4630
+ const tokens = unknown ? Infinity : estimateTokens(visible);
4631
+ estimates.set(message, tokens);
4632
+ return tokens;
4633
+ };
4634
+ const bodies = new Map(effectiveEntries.map((entry) => [entry.id, entry]));
4635
+ const effectiveSummaries = sourceSummaries.map((summary) => {
4636
+ const entry = bodies.get(summary.id);
4637
+ return { ...summary, estTokens: entry?.message ? estimate(entry.message) : summary.type === "message" ? Infinity : summary.estTokens };
4638
+ });
4639
+ const inputEstimate = summaryInput.reduce((sum, message) => sum + estimate(message), 0);
4640
+ const inputTokens = Number.isFinite(inputEstimate) ? inputEstimate : calculateTotalTokens(summaryInput);
4618
4641
  const reductionLimit = inputTokens >= 1e3 ? inputTokens - Math.ceil(inputTokens * 0.1) : Infinity;
4619
4642
  const contextLimit = Math.min(
4620
4643
  reductionLimit,
@@ -4628,7 +4651,7 @@ function prepareSessionCompaction(params) {
4628
4651
  let planned = planRetainedTail(effectiveSummaries, { ...params.compactionConfig, keepRecentTokens: tailBudget });
4629
4652
  if (planned.retainedTail.kind !== "none") {
4630
4653
  const validation = validateRetainedTailEntries({ entries: effectiveEntries, retainedTail: planned.retainedTail });
4631
- planned = validation.ok ? { ...planned, entryIds: validation.entryIds, tailTokens: validation.tailTokens } : { retainedTail: { kind: "none" }, entryIds: [], tailTokens: 0, reason: "pairing" };
4654
+ planned = validation.ok ? { ...planned, entryIds: validation.entryIds, tailTokens: estimateRetainedTailTokens(effectiveSummaries, planned.retainedTail) } : { retainedTail: { kind: "none" }, entryIds: [], tailTokens: 0, reason: "pairing" };
4632
4655
  }
4633
4656
  return {
4634
4657
  sessionId: sessionManager.getActiveSessionId(),
@@ -4684,7 +4707,7 @@ function verifyPreparedCompactionCommit(params) {
4684
4707
  const gain = estimateCompactionGain({
4685
4708
  summary: checkpointSummary,
4686
4709
  tailTokens: estimateRetainedTailTokens(params.preparation.summaries, retainedTail),
4687
- tokensBefore: params.result.tokensBefore ?? params.preparation.inputTokens,
4710
+ tokensBefore: params.preparation.inputTokens,
4688
4711
  budgetTokens: params.preparation.budgetTokens
4689
4712
  });
4690
4713
  if (!gain.ok) {
@@ -5164,7 +5187,13 @@ async function preparePromptMessages(params) {
5164
5187
 
5165
5188
  // ../../packages/agent-framework/src/session/controller/pre-request-policy.ts
5166
5189
  async function runPreRequestAutoCompact(params) {
5167
- if (params.disposed || params.runtimeGuardTriggered || params.isStreaming || params.recoveryActive || params.agent.state.messages.length === 0) {
5190
+ const boundary = params.toolContinuation;
5191
+ const cancelled = () => params.isCurrent?.() === false || boundary?.signal.aborted === true;
5192
+ if (boundary) {
5193
+ await boundary.sync();
5194
+ if (cancelled()) return;
5195
+ }
5196
+ if (params.disposed || params.runtimeGuardTriggered || params.isStreaming && !boundary || params.recoveryActive() || params.agent.state.messages.length === 0) {
5168
5197
  return;
5169
5198
  }
5170
5199
  const decision = params.getAutoCompactDecision?.({
@@ -5173,12 +5202,13 @@ async function runPreRequestAutoCompact(params) {
5173
5202
  cwd: params.cwd,
5174
5203
  messages: params.agent.state.messages.slice()
5175
5204
  });
5176
- if (decision?.reason !== "threshold") {
5205
+ if (cancelled() || decision?.reason !== "threshold") {
5177
5206
  return;
5178
5207
  }
5179
5208
  try {
5180
5209
  await params.runAutoCompact();
5181
- } catch {
5210
+ } catch (error) {
5211
+ if (boundary) throw error;
5182
5212
  if (params.isCurrent?.() !== false) params.setRecoveryState("none");
5183
5213
  }
5184
5214
  }
@@ -5432,12 +5462,7 @@ var AgentSessionQueueDispatcher = class {
5432
5462
  return false;
5433
5463
  }
5434
5464
  this.options.emit({ type: "queued_turn_dispatched", queuedInput: cloneQueuedInput(dispatched) });
5435
- if (dispatched.deliveredToAgent || this.options.agent.state.messages.at(-1)?.role === "assistant") {
5436
- if (!dispatched.deliveredToAgent) {
5437
- this.options.queue.deliverToAgent(dispatched);
5438
- }
5439
- await this.options.continueTurn();
5440
- } else {
5465
+ if (!dispatched.deliveredToAgent) {
5441
5466
  const prepared = await this.options.preparePromptMessages(dispatched.messages, {
5442
5467
  source: dispatched.source,
5443
5468
  streamingBehavior: dispatched.mode,
@@ -5447,13 +5472,20 @@ var AgentSessionQueueDispatcher = class {
5447
5472
  visibility: dispatched.visibility
5448
5473
  });
5449
5474
  if (generation !== this.generation) return false;
5450
- if (!prepared.handled) {
5451
- if (prepared.systemPrompt !== void 0) {
5452
- this.options.agent.setSystemPrompt(prepared.systemPrompt);
5453
- }
5454
- await this.options.agent.prompt(prepared.messages);
5455
- await this.options.syncNewAgentMessages();
5475
+ if (prepared.handled) return true;
5476
+ if (prepared.systemPrompt !== void 0) {
5477
+ this.options.agent.setSystemPrompt(prepared.systemPrompt);
5456
5478
  }
5479
+ dispatched.messages = prepared.messages;
5480
+ }
5481
+ if (dispatched.deliveredToAgent || this.options.agent.state.messages.at(-1)?.role === "assistant") {
5482
+ if (!dispatched.deliveredToAgent) {
5483
+ this.options.queue.deliverToAgent(dispatched);
5484
+ }
5485
+ await this.options.continueTurn();
5486
+ } else {
5487
+ await this.options.agent.prompt(dispatched.messages);
5488
+ await this.options.syncNewAgentMessages();
5457
5489
  }
5458
5490
  return true;
5459
5491
  }
@@ -5574,6 +5606,95 @@ var ReplayStallCircuit = class {
5574
5606
  }
5575
5607
  };
5576
5608
 
5609
+ // ../../packages/agent-framework/src/session/controller/retry-decision.ts
5610
+ function getAgentSessionRetryLimit(retry, failure) {
5611
+ return Math.min(
5612
+ (failure !== void 0 ? retry?.maxRetriesForFailure?.(failure) : void 0) ?? retry?.maxRetries ?? 3,
5613
+ failure?.kind === "provider_confirmation" ? 1 : Infinity
5614
+ );
5615
+ }
5616
+ var AgentSessionRetryEpisode = class {
5617
+ constructor(retry) {
5618
+ this.retry = retry;
5619
+ this.replayCircuit = new ReplayStallCircuit(retry?.systemicReplayStallLimit);
5620
+ }
5621
+ retry;
5622
+ retryAttempts = /* @__PURE__ */ new Map();
5623
+ total = 0;
5624
+ replayCircuit;
5625
+ get totalRetryAttempts() {
5626
+ return this.total;
5627
+ }
5628
+ /** A healthy response ends the current episode and proves endpoint liveness. */
5629
+ succeed() {
5630
+ this.replayCircuit.recordHealthyTurn();
5631
+ const attempts = this.total;
5632
+ this.resetAttempts();
5633
+ return attempts;
5634
+ }
5635
+ /** Cancellation/invalidation resets counters, never the endpoint-health circuit. */
5636
+ resetAttempts() {
5637
+ this.retryAttempts.clear();
5638
+ this.total = 0;
5639
+ }
5640
+ next(failure) {
5641
+ if (!this.retry || !failure.retryable) {
5642
+ if (failure.kind !== "unknown" && failure.kind !== "internal_error") {
5643
+ this.replayCircuit.recordHealthyTurn();
5644
+ }
5645
+ const totalRetryAttempts2 = this.total;
5646
+ this.resetAttempts();
5647
+ return { action: "terminal", failure, totalRetryAttempts: totalRetryAttempts2 };
5648
+ }
5649
+ if (failure.replayExempt === true && this.replayCircuit.isOpen) {
5650
+ const openFailure = asTerminalFailure(
5651
+ failure,
5652
+ `${failure.message} (replay circuit open: ${this.replayCircuit.consecutiveTurnStalls} consecutive turns ended by timeout-replay exhaustion)`
5653
+ );
5654
+ const totalRetryAttempts2 = this.total;
5655
+ this.resetAttempts();
5656
+ return { action: "exhausted", failure: openFailure, maxRetries: 0, totalRetryAttempts: totalRetryAttempts2, reason: "circuit_open" };
5657
+ }
5658
+ const maxRetries = getAgentSessionRetryLimit(this.retry, failure);
5659
+ const kindAttempts = (this.retryAttempts.get(failure.kind) ?? 0) + 1;
5660
+ this.retryAttempts.set(failure.kind, kindAttempts);
5661
+ this.total += 1;
5662
+ const totalRetryAttempts = this.total;
5663
+ const totalBudget = this.retry.maxTotalRetries;
5664
+ const kindExhausted = kindAttempts > maxRetries;
5665
+ const totalExhausted = totalBudget !== void 0 && totalRetryAttempts > totalBudget;
5666
+ const delayMs = resolveRetryDelay(
5667
+ (this.retry.baseDelayMs ?? 2e3) * 2 ** (kindAttempts - 1),
5668
+ this.retry.maxDelayMs,
5669
+ failure.retryAfterMs
5670
+ );
5671
+ if (kindExhausted || totalExhausted || delayMs === void 0) {
5672
+ const terminalFailure = asTerminalFailure(
5673
+ failure,
5674
+ delayMs === void 0 ? `${failure.message} (Retry-After exceeds the allowed wait; automatic recovery paused)` : failure.message
5675
+ );
5676
+ const circuit = failure.replayExempt === true ? this.replayCircuit.recordReplayExhaustion() : void 0;
5677
+ this.resetAttempts();
5678
+ return {
5679
+ action: "exhausted",
5680
+ failure: terminalFailure,
5681
+ maxRetries: totalExhausted ? totalBudget : maxRetries,
5682
+ totalRetryAttempts,
5683
+ reason: delayMs === void 0 ? "retry_after" : totalExhausted ? "total_budget" : "kind_budget",
5684
+ ...circuit ? { circuit } : {}
5685
+ };
5686
+ }
5687
+ return {
5688
+ action: "retry",
5689
+ failure,
5690
+ attempt: kindAttempts,
5691
+ maxRetries,
5692
+ delayMs,
5693
+ totalRetryAttempts
5694
+ };
5695
+ }
5696
+ };
5697
+
5577
5698
  // ../../packages/agent-framework/src/session/controller/retry-policy.ts
5578
5699
  function defaultSetTimeout4(callback, delay) {
5579
5700
  return setTimeout(callback, delay);
@@ -5588,21 +5709,14 @@ var AgentSessionRetryController = class {
5588
5709
  this.removeTrailingAssistantError = removeTrailingAssistantError2;
5589
5710
  this.continueTurn = continueTurn;
5590
5711
  this.setRecoveryState = setRecoveryState;
5591
- this.replayCircuit = new ReplayStallCircuit(retry?.systemicReplayStallLimit);
5712
+ this.episode = new AgentSessionRetryEpisode(retry);
5592
5713
  }
5593
5714
  retry;
5594
5715
  emit;
5595
5716
  removeTrailingAssistantError;
5596
5717
  continueTurn;
5597
5718
  setRecoveryState;
5598
- /** Attempts consumed per failure KIND: budgets are granted per kind (a
5599
- * 10-attempt network budget must not be eaten by interleaved provider
5600
- * errors, and vice versa), so each kind counts its own attempts. */
5601
- retryAttempts = /* @__PURE__ */ new Map();
5602
- /** Total attempts across kinds in this recovery episode, for reporting. */
5603
- totalRetryAttempts = 0;
5604
- /** L2 systemic-stall circuit (see replay-stall-circuit.ts). */
5605
- replayCircuit;
5719
+ episode;
5606
5720
  retryTimer;
5607
5721
  retryEpoch = 0;
5608
5722
  pendingRetry;
@@ -5616,72 +5730,45 @@ var AgentSessionRetryController = class {
5616
5730
  waitForRetry() {
5617
5731
  return this.pendingRetry ?? Promise.resolve();
5618
5732
  }
5619
- /** L2 circuit: healthy turn (success or an actively-answered failure). */
5620
- noteEndpointHealthy() {
5621
- this.replayCircuit.recordHealthyTurn();
5622
- }
5623
5733
  async handleTurnEnd(message, failure) {
5624
5734
  if (!this.retry || !message || message.role !== "assistant") {
5625
5735
  this.settleStrandedRetry(failure);
5626
5736
  return;
5627
5737
  }
5628
5738
  if (!failure) {
5629
- this.noteEndpointHealthy();
5630
- if (this.totalRetryAttempts > 0) {
5631
- this.emit({ type: "retry_succeeded", attempts: this.totalRetryAttempts });
5632
- this.emit({ type: "turn_retry_succeeded", attempts: this.totalRetryAttempts });
5633
- this.resetAttempts();
5739
+ const attempts = this.episode.succeed();
5740
+ if (attempts > 0) {
5741
+ this.emit({ type: "retry_succeeded", attempts });
5742
+ this.emit({ type: "turn_retry_succeeded", attempts });
5634
5743
  this.setRecoveryState("none");
5635
5744
  this.resolvePending();
5636
5745
  }
5637
5746
  return;
5638
5747
  }
5639
- if (!failure.retryable) {
5640
- if (failure.kind !== "unknown" && failure.kind !== "internal_error") {
5641
- this.noteEndpointHealthy();
5642
- }
5643
- if (this.totalRetryAttempts > 0) {
5644
- this.resetAttempts();
5748
+ const decision = this.episode.next(failure);
5749
+ if (decision.action === "terminal") {
5750
+ if (decision.totalRetryAttempts > 0) {
5645
5751
  this.setRecoveryState("none");
5646
5752
  this.resolvePending();
5647
5753
  }
5648
5754
  return;
5649
5755
  }
5650
- if (failure.replayExempt === true && this.replayCircuit.isOpen) {
5651
- const openFailure = asTerminalFailure(failure, `${failure.message} (replay circuit open: ${this.replayCircuit.consecutiveTurnStalls} consecutive turns ended by timeout-replay exhaustion)`);
5652
- this.emit({ type: "retry_exhausted", errorMessage: openFailure.message, maxRetries: 0 });
5653
- this.emit({ type: "turn_failed_terminal", failure: openFailure, message });
5654
- this.emit({ type: "turn_settled" });
5655
- this.resetAttempts();
5656
- this.setRecoveryState("terminal");
5657
- this.resolvePending();
5658
- return;
5659
- }
5660
- const maxRetries = Math.min(this.retry.maxRetriesForFailure?.(failure) ?? this.retry.maxRetries ?? 3, failure.kind === "provider_confirmation" ? 1 : Infinity);
5661
- const kindAttempts = (this.retryAttempts.get(failure.kind) ?? 0) + 1;
5662
- this.retryAttempts.set(failure.kind, kindAttempts);
5663
- this.totalRetryAttempts += 1;
5664
- const totalBudget = this.retry.maxTotalRetries;
5665
- const kindExhausted = kindAttempts > maxRetries;
5666
- const totalExhausted = totalBudget !== void 0 && this.totalRetryAttempts > totalBudget;
5667
- const delayMs = resolveRetryDelay((this.retry.baseDelayMs ?? 2e3) * 2 ** (kindAttempts - 1), this.retry.maxDelayMs, failure.retryAfterMs);
5668
- if (kindExhausted || totalExhausted || delayMs === void 0) {
5669
- const terminalFailure = asTerminalFailure(failure, delayMs === void 0 ? `${failure.message} (Retry-After exceeds the allowed wait; automatic recovery paused)` : failure.message);
5670
- if (failure.replayExempt === true) {
5671
- const outcome = this.replayCircuit.recordReplayExhaustion();
5672
- if (outcome.opened) {
5673
- this.emit({ type: "retry_exhausted", errorMessage: `systemic timeout-stall: ${outcome.consecutive} consecutive turns ended by timeout-replay exhaustion`, maxRetries: totalExhausted ? totalBudget : maxRetries });
5674
- }
5756
+ if (decision.action === "exhausted") {
5757
+ if (decision.circuit?.opened) {
5758
+ this.emit({
5759
+ type: "retry_exhausted",
5760
+ errorMessage: `systemic timeout-stall: ${decision.circuit.consecutive} consecutive turns ended by timeout-replay exhaustion`,
5761
+ maxRetries: decision.maxRetries
5762
+ });
5675
5763
  }
5676
- this.emit({ type: "retry_exhausted", errorMessage: terminalFailure.message, maxRetries: totalExhausted ? totalBudget : maxRetries });
5677
- this.emit({ type: "turn_failed_terminal", failure: terminalFailure, message });
5764
+ this.emit({ type: "retry_exhausted", errorMessage: decision.failure.message, maxRetries: decision.maxRetries });
5765
+ this.emit({ type: "turn_failed_terminal", failure: decision.failure, message });
5678
5766
  this.emit({ type: "turn_settled" });
5679
- this.resetAttempts();
5680
5767
  this.setRecoveryState("terminal");
5681
5768
  this.resolvePending();
5682
5769
  return;
5683
5770
  }
5684
- const attempt = kindAttempts;
5771
+ const { attempt, maxRetries, delayMs } = decision;
5685
5772
  this.ensurePending();
5686
5773
  const epoch = this.retryEpoch;
5687
5774
  const pending = this.pendingRetry;
@@ -5708,7 +5795,7 @@ var AgentSessionRetryController = class {
5708
5795
  this.emit({ type: "retry_exhausted", errorMessage, maxRetries });
5709
5796
  this.emit({ type: "turn_failed_terminal", failure: terminalFailure, message });
5710
5797
  this.emit({ type: "turn_settled" });
5711
- this.resetAttempts();
5798
+ this.episode.resetAttempts();
5712
5799
  this.setRecoveryState("terminal");
5713
5800
  this.resolvePending(pending);
5714
5801
  });
@@ -5733,24 +5820,20 @@ var AgentSessionRetryController = class {
5733
5820
  userAbort: false,
5734
5821
  terminal: true
5735
5822
  });
5736
- const maxRetries = Math.min((failure !== void 0 ? this.retry?.maxRetriesForFailure?.(failure) : void 0) ?? this.retry?.maxRetries ?? 3, failure?.kind === "provider_confirmation" ? 1 : Infinity);
5823
+ const maxRetries = getAgentSessionRetryLimit(this.retry, failure);
5737
5824
  this.emit({ type: "retry_exhausted", errorMessage: terminalFailure.message, maxRetries });
5738
5825
  this.emit({ type: "turn_failed_terminal", failure: terminalFailure });
5739
5826
  this.emit({ type: "turn_settled" });
5740
- this.resetAttempts();
5827
+ this.episode.resetAttempts();
5741
5828
  this.setRecoveryState("terminal");
5742
5829
  this.resolvePending();
5743
5830
  }
5744
5831
  cancel() {
5745
5832
  this.retryEpoch++;
5746
5833
  this.cancelTimerOnly();
5747
- this.resetAttempts();
5834
+ this.episode.resetAttempts();
5748
5835
  this.resolvePending();
5749
5836
  }
5750
- resetAttempts() {
5751
- this.retryAttempts.clear();
5752
- this.totalRetryAttempts = 0;
5753
- }
5754
5837
  cancelTimerOnly() {
5755
5838
  if (this.retryTimer === void 0) {
5756
5839
  return;
@@ -6089,7 +6172,8 @@ async function runAgentSessionCompaction(params) {
6089
6172
  const preparation = prepareSessionCompaction({
6090
6173
  sessionManager: params.sessionManager,
6091
6174
  messages: params.agent.state.messages,
6092
- budgetTokens: params.compactionBudgetTokens?.()
6175
+ budgetTokens: params.compactionBudgetTokens?.(),
6176
+ modelView: params.compaction.modelView
6093
6177
  });
6094
6178
  let result = await params.compaction.compact({
6095
6179
  reason: params.reason,
@@ -6368,6 +6452,8 @@ var AgentSessionController = class {
6368
6452
  autoContinue;
6369
6453
  unsubscribeAgent;
6370
6454
  unsubscribeSemantic;
6455
+ unsubscribeToolContinuation;
6456
+ compactionOwner;
6371
6457
  lastSyncedMessageCount = 0;
6372
6458
  disposed = false;
6373
6459
  transitioning = false;
@@ -6471,6 +6557,7 @@ var AgentSessionController = class {
6471
6557
  });
6472
6558
  this.syncFromActiveSession();
6473
6559
  this.unsubscribeAgent = this.agent.subscribe((event) => this.handleAgentEvent(event));
6560
+ this.unsubscribeToolContinuation = this.agent.setBeforeToolContinuation((signal) => this.runPreRequestAutoCompactIfNeeded(signal));
6474
6561
  this.unsubscribeSemantic = this.agent.subscribeSemantic((event) => {
6475
6562
  this.emit({ type: "semantic_event", event });
6476
6563
  });
@@ -6521,6 +6608,7 @@ var AgentSessionController = class {
6521
6608
  this.transitioning = true;
6522
6609
  try {
6523
6610
  this.generation++;
6611
+ if (this.isStreaming) this.agent.resetProcessingState();
6524
6612
  this.cancelScheduledWork();
6525
6613
  this.recoveryState = "none";
6526
6614
  this.pendingCompactionFailure = void 0;
@@ -6652,48 +6740,61 @@ var AgentSessionController = class {
6652
6740
  const generation = this.generation;
6653
6741
  if (this.disposed || this.transitioning) throw new Error("Controller disposed or transitioning");
6654
6742
  const isCurrent = () => generation === this.generation && !this.disposed;
6655
- return this.convergence.run(() => runAgentSessionControllerAutoCompact({
6656
- isCurrent,
6657
- reason,
6658
- willRetry,
6659
- signal: options?.signal,
6660
- compaction: this.compaction,
6661
- agent: this.agent,
6662
- sessionManager: this.sessionManager,
6663
- cwd: this.cwd,
6664
- pendingFailure: this.pendingCompactionFailure,
6665
- emit: (event) => {
6666
- if (isCurrent()) this.emit(event);
6667
- },
6668
- setRecoveryState: (state) => {
6669
- this.recoveryState = state;
6670
- },
6671
- getRecoveryState: () => this.recoveryState,
6672
- getDisposed: () => this.disposed,
6673
- cancelRetry: () => this.retryController.cancel(),
6674
- clearPendingFailure: () => {
6675
- this.pendingCompactionFailure = void 0;
6676
- },
6677
- getCompactionBudgetTokens: this.compactionSettings.getCompactionBudgetTokens,
6678
- removeTrailingAssistantError: () => this.removeTrailingAssistantError(),
6679
- replaceMessagesAndMarkSynced: (messages) => this.replaceMessagesAndMarkSynced(messages),
6680
- markSyncedToAgentState: () => this.markSyncedToAgentState(),
6681
- continueTurn: () => this.continue(),
6682
- handleAgentEndHook: (messages) => this.handleAgentEndHook(messages),
6683
- emitSettled: options?.emitSettled,
6684
- runAfterAgentEndHook: options?.runAfterAgentEndHook,
6685
- cancelOnAbort: options?.cancelOnAbort
6686
- }));
6743
+ if (this.compactionOwner) throw new Error("Compaction already running");
6744
+ const owner = this.compactionOwner = {};
6745
+ try {
6746
+ return await this.convergence.run(() => runAgentSessionControllerAutoCompact({
6747
+ isCurrent,
6748
+ reason,
6749
+ willRetry,
6750
+ signal: options?.signal,
6751
+ compaction: this.compaction,
6752
+ agent: this.agent,
6753
+ sessionManager: this.sessionManager,
6754
+ cwd: this.cwd,
6755
+ pendingFailure: this.pendingCompactionFailure,
6756
+ emit: (event) => {
6757
+ if (isCurrent()) this.emit(event);
6758
+ },
6759
+ setRecoveryState: (state) => {
6760
+ this.recoveryState = state;
6761
+ },
6762
+ getRecoveryState: () => this.recoveryState,
6763
+ getDisposed: () => this.disposed,
6764
+ cancelRetry: () => this.retryController.cancel(),
6765
+ clearPendingFailure: () => {
6766
+ this.pendingCompactionFailure = void 0;
6767
+ },
6768
+ getCompactionBudgetTokens: this.compactionSettings.getCompactionBudgetTokens,
6769
+ removeTrailingAssistantError: () => this.removeTrailingAssistantError(),
6770
+ replaceMessagesAndMarkSynced: (messages) => this.replaceMessagesAndMarkSynced(messages),
6771
+ markSyncedToAgentState: () => this.markSyncedToAgentState(),
6772
+ continueTurn: async () => {
6773
+ if (!isCurrent() || options?.signal?.aborted) return;
6774
+ if (this.compactionOwner === owner) this.compactionOwner = void 0;
6775
+ this.recoveryState = "none";
6776
+ await this.continue();
6777
+ },
6778
+ handleAgentEndHook: (messages) => this.handleAgentEndHook(messages),
6779
+ emitSettled: options?.emitSettled,
6780
+ runAfterAgentEndHook: options?.runAfterAgentEndHook,
6781
+ cancelOnAbort: options?.cancelOnAbort
6782
+ }));
6783
+ } finally {
6784
+ if (this.compactionOwner === owner) this.compactionOwner = void 0;
6785
+ }
6687
6786
  }
6688
6787
  dispose() {
6689
6788
  if (this.disposed) return;
6690
6789
  this.disposed = true;
6790
+ if (this.isStreaming) this.agent.resetProcessingState();
6691
6791
  this.agent.releaseTransport?.();
6692
6792
  this.generation++;
6693
6793
  this.pendingEndOperations.clear();
6694
6794
  this.cancelScheduledWork();
6695
6795
  this.unsubscribeAgent?.();
6696
6796
  this.unsubscribeSemantic?.();
6797
+ this.unsubscribeToolContinuation?.();
6697
6798
  this.queue.clear();
6698
6799
  this.convergence.invalidate("dispose");
6699
6800
  this.listeners.clear();
@@ -6717,19 +6818,26 @@ var AgentSessionController = class {
6717
6818
  });
6718
6819
  }
6719
6820
  }
6720
- async runPreRequestAutoCompactIfNeeded() {
6821
+ async runPreRequestAutoCompactIfNeeded(signal) {
6721
6822
  const generation = this.generation;
6722
6823
  return runPreRequestAutoCompact({
6723
6824
  isCurrent: () => generation === this.generation && !this.disposed,
6724
6825
  disposed: this.disposed,
6725
6826
  runtimeGuardTriggered: this.runtimeGuard.triggered,
6726
6827
  isStreaming: this.isStreaming,
6727
- recoveryActive: this.isRecoveryActive(),
6828
+ recoveryActive: () => this.isRecoveryActive(),
6728
6829
  agent: this.agent,
6729
6830
  sessionManager: this.sessionManager,
6730
6831
  cwd: this.cwd,
6731
6832
  getAutoCompactDecision: this.compactionSettings.getAutoCompactDecision,
6732
- runAutoCompact: () => this.runAutoCompact("threshold", false, { emitSettled: false, runAfterAgentEndHook: false, cancelOnAbort: false }).then(() => void 0),
6833
+ toolContinuation: signal ? { signal, sync: async () => {
6834
+ await this.pendingEndOperations.wait();
6835
+ if (generation === this.generation && !this.disposed && !signal.aborted) await this.syncNewAgentMessages();
6836
+ } } : void 0,
6837
+ runAutoCompact: async () => {
6838
+ const result = await this.runAutoCompact("threshold", false, { signal, emitSettled: false, runAfterAgentEndHook: false, cancelOnAbort: false });
6839
+ if (signal && (!result || result.aborted)) throw new Error("Tool-continuation compaction was aborted");
6840
+ },
6733
6841
  setRecoveryState: (state) => {
6734
6842
  this.recoveryState = state;
6735
6843
  }
@@ -6874,6 +6982,7 @@ var AgentSessionController = class {
6874
6982
  return this.recoveryState !== "none" || this.retryController.hasPendingRetry || this.autoCompact.hasPendingAutoCompact;
6875
6983
  }
6876
6984
  cancelScheduledWork() {
6985
+ this.compactionOwner = void 0;
6877
6986
  this.retryController.cancel();
6878
6987
  this.autoCompact.cancel();
6879
6988
  this.queueDispatcher.cancel();
@@ -8229,6 +8338,9 @@ function withSubagentRunContext(stream, hooks) {
8229
8338
  ${SUBAGENT_FINALIZATION_HINT}` } : context, options);
8230
8339
  };
8231
8340
  }
8341
+ function isRetryableSubagentFailure(message) {
8342
+ return isRetryableSubagentFailureFrom({ errorMessage: message });
8343
+ }
8232
8344
  function isRetryableSubagentFailureFrom(facts) {
8233
8345
  if (facts.retryable === false) return false;
8234
8346
  if (facts.replayClass === "stream_lost" || facts.replayClass === "stream_lost_partial") return true;
@@ -8250,90 +8362,366 @@ function cancellableBackoff(ms, signal) {
8250
8362
  if (signal?.aborted) finish();
8251
8363
  });
8252
8364
  }
8365
+ function factsFromMessage(message) {
8366
+ return {
8367
+ errorMessage: message.errorMessage,
8368
+ retryable: message.retryable,
8369
+ replayClass: message.replayClass,
8370
+ replayEligible: message.replayEligible,
8371
+ remoteFailure: message.remoteFailure,
8372
+ stopReason: message.stopReason
8373
+ };
8374
+ }
8253
8375
  function factsOf(error) {
8254
8376
  if (error instanceof Error) {
8255
8377
  const carrier = error;
8256
- return { errorMessage: error.message, retryable: carrier.retryable, replayClass: carrier.replayClass };
8378
+ return {
8379
+ errorMessage: carrier.subagentFailure?.errorMessage ?? error.message,
8380
+ retryable: carrier.subagentFailure?.retryable ?? carrier.retryable,
8381
+ replayClass: carrier.subagentFailure?.replayClass ?? carrier.replayClass,
8382
+ replayEligible: carrier.subagentFailure?.replayEligible ?? carrier.replayEligible,
8383
+ remoteFailure: carrier.subagentFailure?.remoteFailure ?? carrier.remoteFailure,
8384
+ stopReason: carrier.subagentFailure?.stopReason ?? carrier.stopReason ?? (carrier.subagentFailure?.classification?.userAbort ? "aborted" : "error")
8385
+ };
8257
8386
  }
8258
- return { errorMessage: String(error) };
8387
+ return { errorMessage: String(error), stopReason: "error" };
8259
8388
  }
8260
- function withSubagentResponseRecovery(stream, backoffMs = 2e3, onRetry, onActivity, hooks) {
8389
+ function assistantFromFacts(facts) {
8390
+ return {
8391
+ role: "assistant",
8392
+ api: "unknown",
8393
+ provider: "unknown",
8394
+ model: "unknown",
8395
+ content: [],
8396
+ timestamp: 0,
8397
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
8398
+ stopReason: facts.stopReason ?? "error",
8399
+ errorMessage: facts.errorMessage,
8400
+ ...facts.retryable !== void 0 ? { retryable: facts.retryable } : {},
8401
+ ...facts.replayClass !== void 0 ? { replayClass: facts.replayClass } : {},
8402
+ ...facts.replayEligible !== void 0 ? { replayEligible: facts.replayEligible } : {},
8403
+ ...facts.remoteFailure !== void 0 ? { remoteFailure: facts.remoteFailure } : {}
8404
+ };
8405
+ }
8406
+ function boundedClassification(failure) {
8407
+ return { ...failure, message: failure.message.slice(0, 2e3) };
8408
+ }
8409
+ function failureDetails(facts, classification, recovery) {
8410
+ return {
8411
+ errorMessage: (classification.message || facts.errorMessage || "Subagent response failed").slice(0, 2e3),
8412
+ ...facts.retryable !== void 0 ? { retryable: facts.retryable } : {},
8413
+ ...facts.replayClass !== void 0 ? { replayClass: facts.replayClass } : {},
8414
+ ...facts.replayEligible !== void 0 ? { replayEligible: facts.replayEligible } : {},
8415
+ ...facts.remoteFailure !== void 0 ? {
8416
+ remoteFailure: {
8417
+ category: facts.remoteFailure.category,
8418
+ ...typeof facts.remoteFailure.retryAfterMs === "number" && Number.isFinite(facts.remoteFailure.retryAfterMs) && facts.remoteFailure.retryAfterMs >= 0 ? { retryAfterMs: facts.remoteFailure.retryAfterMs } : {}
8419
+ }
8420
+ } : {},
8421
+ ...facts.stopReason === "aborted" ? { stopReason: facts.stopReason } : {},
8422
+ classification: boundedClassification(classification),
8423
+ recovery
8424
+ };
8425
+ }
8426
+ function legacyRecovery(backoffMs) {
8427
+ return {
8428
+ policy: {
8429
+ isRetryableError: (message) => isRetryableSubagentFailure(message),
8430
+ maxRetries: 2,
8431
+ baseDelayMs: backoffMs
8432
+ },
8433
+ classifyFailure: (context) => {
8434
+ const facts = context.message?.role === "assistant" ? factsFromMessage(context.message) : { errorMessage: context.errorMessage, stopReason: "error" };
8435
+ const retryable = isRetryableSubagentFailureFrom({ ...facts, errorMessage: context.errorMessage });
8436
+ return {
8437
+ kind: retryable ? "provider_transient" : "unknown",
8438
+ retryable,
8439
+ compactable: false,
8440
+ userAbort: false,
8441
+ terminal: !retryable,
8442
+ message: context.errorMessage
8443
+ };
8444
+ }
8445
+ };
8446
+ }
8447
+ function resolveRecovery(options, backoffMs) {
8448
+ if (!options) return legacyRecovery(backoffMs);
8449
+ const fallback = legacyRecovery(backoffMs);
8450
+ return {
8451
+ policy: options.policy ?? fallback.policy,
8452
+ // An explicitly supplied policy without a classifier must reach its own
8453
+ // isRetryableError predicate, exactly as in the main controller. Only the
8454
+ // no-host compatible default installs the legacy prose classifier.
8455
+ classifyFailure: options.classifyFailure ?? (options.policy ? void 0 : fallback.classifyFailure)
8456
+ };
8457
+ }
8458
+ function classify(facts, recovery) {
8459
+ const message = assistantFromFacts(facts);
8460
+ return createFailureClassification({
8461
+ message,
8462
+ errorMessage: facts.errorMessage ?? "Subagent response failed",
8463
+ phase: "stream"
8464
+ }, recovery.policy, recovery.classifyFailure);
8465
+ }
8466
+ var DEFAULT_ITERATOR_CLEANUP_MS = 2e3;
8467
+ async function closeIterator(iterator, hooks, signal) {
8468
+ const cleanup = Promise.resolve().then(() => iterator.return?.()).then(() => void 0).catch(() => void 0);
8469
+ const remaining = hooks?.hardDeadlineAt === void 0 ? DEFAULT_ITERATOR_CLEANUP_MS : Math.max(0, hooks.hardDeadlineAt - Date.now());
8470
+ const waitMs = Math.min(DEFAULT_ITERATOR_CLEANUP_MS, remaining);
8471
+ if (waitMs <= 0 || signal?.aborted) return;
8472
+ let timer;
8473
+ let onAbort;
8474
+ try {
8475
+ await Promise.race([
8476
+ cleanup,
8477
+ new Promise((resolve2) => {
8478
+ timer = setTimeout(resolve2, waitMs);
8479
+ onAbort = () => resolve2();
8480
+ signal?.addEventListener("abort", onAbort, { once: true });
8481
+ })
8482
+ ]);
8483
+ } finally {
8484
+ if (timer !== void 0) clearTimeout(timer);
8485
+ if (onAbort) signal?.removeEventListener("abort", onAbort);
8486
+ }
8487
+ }
8488
+ function singleResponse(message) {
8489
+ const result = new AssistantMessageEventStream();
8490
+ if (message.stopReason === "error" || message.stopReason === "aborted") {
8491
+ result.push({ type: "error", reason: message.stopReason, error: { ...message, content: [] } });
8492
+ } else {
8493
+ result.push({ type: "done", reason: message.stopReason, message });
8494
+ }
8495
+ result.end();
8496
+ return result;
8497
+ }
8498
+ function throwWithDetails(error, details) {
8499
+ const failure = new Error(details.classification?.message ?? (error instanceof Error ? error.message : String(error)));
8500
+ failure.cause = error;
8501
+ failure.subagentFailure = details;
8502
+ throw failure;
8503
+ }
8504
+ function withSubagentResponseRecovery(stream, backoffMs = 2e3, onRetry, onActivity, hooks, recoveryOptions) {
8505
+ const recovery = resolveRecovery(recoveryOptions, backoffMs);
8506
+ const episode = recoveryOptions?.episode ?? hooks?.recoveryEpisode ?? new AgentSessionRetryEpisode(recovery.policy);
8507
+ if (hooks) hooks.recoveryEpisode = episode;
8261
8508
  return async (model, context, options) => {
8262
- for (let attempt = 0; ; attempt++) {
8263
- if (hooks) {
8264
- hooks.terminalText = void 0;
8265
- hooks.terminalStopReason = void 0;
8266
- }
8267
- if (options?.signal?.aborted) throw new Error("Subagent aborted by parent");
8268
- if (hooks?.hardDeadlineAt !== void 0 && Date.now() >= hooks.hardDeadlineAt) throw new Error("Subagent hard deadline reached");
8269
- let message;
8270
- const finishUsage = hooks?.usage ? beginSubagentUsage(hooks.usage, model) : void 0;
8271
- try {
8272
- const response = await stream(model, context, options);
8273
- let terminal;
8274
- const iterator = response[Symbol.asyncIterator]();
8509
+ let attempts = 0;
8510
+ let retries = 0;
8511
+ if (hooks) hooks.failure = void 0;
8512
+ try {
8513
+ for (; ; ) {
8514
+ if (hooks) {
8515
+ hooks.terminalText = void 0;
8516
+ hooks.terminalStopReason = void 0;
8517
+ }
8518
+ if (options?.signal?.aborted) {
8519
+ episode.resetAttempts();
8520
+ throw new Error("Subagent aborted by parent");
8521
+ }
8522
+ if (hooks?.hardDeadlineAt !== void 0 && Date.now() >= hooks.hardDeadlineAt) throw new Error("Subagent hard deadline reached");
8523
+ let message;
8524
+ const finishUsage = hooks?.usage ? beginSubagentUsage(hooks.usage, model) : void 0;
8525
+ attempts += 1;
8275
8526
  try {
8276
- while (true) {
8277
- const next = await iterator.next();
8278
- if (next.done) break;
8279
- try {
8280
- onActivity?.(next.value.type);
8281
- } catch {
8282
- }
8283
- if (next.value.type === "done") {
8284
- terminal = next.value.message;
8285
- finishUsage?.(terminal);
8286
- }
8287
- if (next.value.type === "error") {
8288
- terminal = next.value.error;
8289
- finishUsage?.(terminal);
8290
- }
8291
- if (hooks && (next.value.type === "done" || next.value.type === "error")) {
8292
- const reportTerminal = terminal && ["stop", "length", "error", "aborted"].includes(terminal.stopReason) && !terminal.content.some((block) => block.type === "toolCall");
8293
- hooks.terminalStopReason = reportTerminal && terminal ? terminal.stopReason : void 0;
8294
- hooks.terminalText = reportTerminal && terminal ? terminal.content.filter((block) => block.type === "text").map((block) => block.text).join("\n").slice(0, 24001) : void 0;
8527
+ const response = await stream(model, context, options);
8528
+ let terminal;
8529
+ const iterator = response[Symbol.asyncIterator]();
8530
+ try {
8531
+ for (; ; ) {
8532
+ const next = await iterator.next();
8533
+ if (next.done) break;
8534
+ try {
8535
+ onActivity?.(next.value.type);
8536
+ } catch {
8537
+ }
8538
+ if (next.value.type === "done") {
8539
+ terminal = next.value.message;
8540
+ finishUsage?.(terminal);
8541
+ }
8542
+ if (next.value.type === "error") {
8543
+ terminal = next.value.error;
8544
+ finishUsage?.(terminal);
8545
+ }
8546
+ if (next.value.type === "done" || next.value.type === "error") {
8547
+ if (hooks && terminal) {
8548
+ const reportTerminal = ["stop", "length", "error", "aborted"].includes(terminal.stopReason) && !terminal.content.some((block) => block.type === "toolCall");
8549
+ hooks.terminalStopReason = reportTerminal ? terminal.stopReason : void 0;
8550
+ hooks.terminalText = reportTerminal ? terminal.content.filter((block) => block.type === "text").map((block) => block.text).join("\n").slice(0, 24001) : void 0;
8551
+ }
8552
+ break;
8553
+ }
8295
8554
  }
8555
+ } finally {
8556
+ await closeIterator(iterator, hooks, options?.signal);
8296
8557
  }
8297
- } finally {
8298
- try {
8299
- await iterator.return?.();
8300
- } catch {
8558
+ if (!terminal) throw new Error("Provider protocol error: response ended without a terminal event");
8559
+ message = terminal;
8560
+ } catch (error) {
8561
+ finishUsage?.();
8562
+ if (options?.signal?.aborted) {
8563
+ episode.resetAttempts();
8564
+ throw error;
8301
8565
  }
8302
- }
8303
- if (!terminal) throw new Error("Provider protocol error: response ended without a terminal event");
8304
- message = terminal;
8305
- } catch (error) {
8306
- finishUsage?.();
8307
- if (!options?.signal?.aborted && attempt < 2 && isRetryableSubagentFailureFrom(factsOf(error))) {
8308
- try {
8309
- onRetry?.();
8310
- } catch {
8566
+ const facts2 = factsOf(error);
8567
+ if (facts2.stopReason === "aborted") {
8568
+ episode.resetAttempts();
8569
+ const classification3 = {
8570
+ kind: "user_abort",
8571
+ retryable: false,
8572
+ compactable: false,
8573
+ userAbort: true,
8574
+ terminal: true,
8575
+ message: facts2.errorMessage ?? "Subagent aborted"
8576
+ };
8577
+ const details = failureDetails(facts2, classification3, { outcome: "terminal", attempts, retries, reason: "cancelled" });
8578
+ if (hooks) hooks.failure = details;
8579
+ throwWithDetails(error, details);
8311
8580
  }
8312
- await cancellableBackoff(backoffMs * (attempt + 1), options?.signal);
8313
- continue;
8581
+ const classification2 = classify(facts2, recovery);
8582
+ const failure2 = await resolveFailure({
8583
+ facts: facts2,
8584
+ classification: classification2,
8585
+ attempts,
8586
+ retries,
8587
+ episode,
8588
+ hooks,
8589
+ onRetry,
8590
+ options
8591
+ });
8592
+ if (failure2.retry) {
8593
+ retries += 1;
8594
+ continue;
8595
+ }
8596
+ throwWithDetails(error, failure2.details);
8314
8597
  }
8315
- throw error;
8316
- }
8317
- if (options?.signal?.aborted) throw new Error("Subagent aborted by parent");
8318
- if (message.stopReason === "error" && attempt < 2 && isRetryableSubagentFailureFrom({ errorMessage: message.errorMessage, retryable: message.retryable, replayClass: message.replayClass })) {
8319
- try {
8320
- onRetry?.();
8321
- } catch {
8598
+ if (options?.signal?.aborted) {
8599
+ episode.resetAttempts();
8600
+ throw new Error("Subagent aborted by parent");
8322
8601
  }
8323
- await cancellableBackoff(backoffMs * (attempt + 1), options?.signal);
8324
- continue;
8325
- }
8326
- const result = new AssistantMessageEventStream();
8327
- if (message.stopReason === "error" || message.stopReason === "aborted") {
8328
- result.push({ type: "error", reason: message.stopReason, error: { ...message, content: [] } });
8329
- } else {
8330
- result.push({ type: "done", reason: message.stopReason, message });
8602
+ if (hooks?.hardDeadlineAt !== void 0 && Date.now() >= hooks.hardDeadlineAt) {
8603
+ episode.resetAttempts();
8604
+ throw new Error("Subagent hard deadline reached");
8605
+ }
8606
+ if (message.stopReason !== "error" && message.stopReason !== "aborted") {
8607
+ episode.succeed();
8608
+ if (hooks) hooks.failure = void 0;
8609
+ return singleResponse(message);
8610
+ }
8611
+ if (message.stopReason === "aborted") {
8612
+ episode.resetAttempts();
8613
+ if (hooks) hooks.failure = void 0;
8614
+ return singleResponse(message);
8615
+ }
8616
+ const facts = factsFromMessage(message);
8617
+ const classification = classify(facts, recovery);
8618
+ const failure = await resolveFailure({
8619
+ facts,
8620
+ classification,
8621
+ attempts,
8622
+ retries,
8623
+ episode,
8624
+ hooks,
8625
+ onRetry,
8626
+ options
8627
+ });
8628
+ if (failure.retry) {
8629
+ retries += 1;
8630
+ continue;
8631
+ }
8632
+ const finalMessage = failure.details.classification?.message && failure.details.classification.message !== message.errorMessage ? { ...message, errorMessage: failure.details.classification.message } : message;
8633
+ return singleResponse(finalMessage);
8331
8634
  }
8332
- result.end();
8333
- return result;
8635
+ } finally {
8636
+ episode.resetAttempts();
8334
8637
  }
8335
8638
  };
8336
8639
  }
8640
+ async function resolveFailure(params) {
8641
+ const { facts, classification, attempts, retries, episode, hooks, onRetry, options } = params;
8642
+ if (options?.signal?.aborted) {
8643
+ episode.resetAttempts();
8644
+ throw new Error("Subagent aborted by parent");
8645
+ }
8646
+ if (classification.userAbort) {
8647
+ episode.resetAttempts();
8648
+ const cancellation = {
8649
+ ...classification,
8650
+ kind: "user_abort",
8651
+ retryable: false,
8652
+ compactable: false,
8653
+ userAbort: true,
8654
+ terminal: true
8655
+ };
8656
+ const details2 = failureDetails({ ...facts, stopReason: "aborted" }, cancellation, {
8657
+ outcome: "terminal",
8658
+ attempts,
8659
+ retries,
8660
+ reason: "cancelled"
8661
+ });
8662
+ if (hooks) hooks.failure = details2;
8663
+ return { retry: false, details: details2 };
8664
+ }
8665
+ if (classification.compactable) {
8666
+ const details2 = failureDetails(facts, classification, {
8667
+ outcome: "overflow",
8668
+ attempts,
8669
+ retries,
8670
+ reason: "compaction_unavailable"
8671
+ });
8672
+ if (hooks) hooks.failure = details2;
8673
+ return { retry: false, details: details2 };
8674
+ }
8675
+ let decision = episode.next(classification);
8676
+ if (decision.action === "retry") {
8677
+ const remaining = hooks?.hardDeadlineAt === void 0 ? Infinity : hooks.hardDeadlineAt - Date.now();
8678
+ if (remaining <= 0) {
8679
+ episode.resetAttempts();
8680
+ throw new Error("Subagent hard deadline reached");
8681
+ }
8682
+ if (decision.delayMs > remaining) {
8683
+ const deadlineClassification = {
8684
+ ...classification,
8685
+ retryable: false,
8686
+ terminal: true,
8687
+ message: `${classification.message} (recovery delay ${decision.delayMs}ms exceeds the remaining subagent deadline ${Math.max(0, Math.round(remaining))}ms)`
8688
+ };
8689
+ const details2 = failureDetails(facts, deadlineClassification, {
8690
+ outcome: "exhausted",
8691
+ attempts,
8692
+ retries,
8693
+ maxRetries: decision.maxRetries,
8694
+ reason: "deadline"
8695
+ });
8696
+ if (hooks) hooks.failure = details2;
8697
+ return { retry: false, details: details2 };
8698
+ }
8699
+ try {
8700
+ onRetry?.();
8701
+ } catch {
8702
+ }
8703
+ await cancellableBackoff(decision.delayMs, options?.signal);
8704
+ if (options?.signal?.aborted) {
8705
+ episode.resetAttempts();
8706
+ throw new Error("Subagent aborted by parent");
8707
+ }
8708
+ if (hooks?.hardDeadlineAt !== void 0 && Date.now() >= hooks.hardDeadlineAt) {
8709
+ episode.resetAttempts();
8710
+ throw new Error("Subagent hard deadline reached");
8711
+ }
8712
+ return { retry: true };
8713
+ }
8714
+ const finalClassification = decision.action === "exhausted" ? decision.failure : classification;
8715
+ const details = failureDetails(facts, finalClassification, {
8716
+ outcome: decision.action === "exhausted" ? "exhausted" : "terminal",
8717
+ attempts,
8718
+ retries,
8719
+ ...decision.action !== "terminal" ? { maxRetries: decision.maxRetries } : {},
8720
+ ...decision.action === "exhausted" ? { reason: decision.reason } : {}
8721
+ });
8722
+ if (hooks) hooks.failure = details;
8723
+ return { retry: false, details };
8724
+ }
8337
8725
 
8338
8726
  // ../../packages/shared-headless-capabilities/src/subagent-sessions.ts
8339
8727
  var SUBAGENT_SESSION_RECORD_VERSION = 1;
@@ -8467,8 +8855,11 @@ var POWERSHELL_DESTRUCTIVE_PATTERNS = [
8467
8855
  // PowerShell resolves these aliases before execution. The classifier must
8468
8856
  // treat the aliases exactly like their canonical cmdlets; otherwise a
8469
8857
  // guarded shell can run `ri -Recurse -Force` without the confirmation that
8470
- // `Remove-Item -Recurse -Force` receives.
8471
- { pattern: /\b(?:Remove-Item|ri|rm|rmdir|del|erase|rd)\b[^;\n|]*(?:-Recurse|-Force)\b/i, warning: "may recursively or forcefully remove files", commandClass: "filesystem_write" },
8858
+ // `Remove-Item -Recurse -Force` receives. PowerShell parameters are
8859
+ // prefix-abbreviatable (`-r` == `-Recurse`, `-fo` == `-Force`), so the
8860
+ // parameter matcher below accepts any unambiguous-style abbreviation,
8861
+ // not just the full spellings.
8862
+ { pattern: buildPowerShellRemoveItemPattern(), warning: "may recursively or forcefully remove files", commandClass: "filesystem_write" },
8472
8863
  { pattern: /\b(?:Remove-Item|ri|rm|rmdir|del|erase|rd)\b[^;\n|]*(?:HKLM:|HKCU:|Registry::)/i, warning: "may remove registry entries", commandClass: "infra_destructive" },
8473
8864
  // These write aliases are not all destructive in the recursive-delete sense,
8474
8865
  // but they can overwrite or create arbitrary files. Keep them behind the
@@ -8482,7 +8873,10 @@ var POWERSHELL_DESTRUCTIVE_PATTERNS = [
8482
8873
  ];
8483
8874
  var CMD_DESTRUCTIVE_PATTERNS = [
8484
8875
  { pattern: /\b(del|erase)\b[^&|\n]*(\/s|\/q)/i, warning: "may recursively or quietly delete files", commandClass: "filesystem_write" },
8485
- { pattern: /\brmdir\b[^&|\n]*(\/s|\/q)/i, warning: "may recursively remove directories", commandClass: "filesystem_write" },
8876
+ // `rd` is cmd's documented synonym for rmdir and accepts the same /s /q
8877
+ // switches; classifying only `rmdir` let `rd /s /q C:\\anywhere` run as an
8878
+ // undetected recursive deletion.
8879
+ { pattern: /\b(?:rmdir|rd)\b[^&|\n]*(\/s|\/q)/i, warning: "may recursively remove directories", commandClass: "filesystem_write" },
8486
8880
  { pattern: /\bformat\s+[a-z]:/i, warning: "may format a drive", commandClass: "infra_destructive" },
8487
8881
  { pattern: /\b(takeown|icacls)\b[^&|\n]*(\/grant|\/reset|\/remove)/i, warning: "may alter ownership or permissions", commandClass: "privilege_escalation" },
8488
8882
  { pattern: /\bgit\s+reset\s+--hard\b/i, warning: "may discard uncommitted changes", commandClass: "git_destructive" },
@@ -8494,18 +8888,27 @@ function splitCommand(command) {
8494
8888
  function firstWord(command) {
8495
8889
  return command.trim().split(/\s+/)[0] || "";
8496
8890
  }
8891
+ function buildPowerShellRemoveItemPattern() {
8892
+ const removeVerb = "(?:Remove-Item|ri|rm|rmdir|del|erase|rd)";
8893
+ const recurseg = "-(?:r(?:e(?:c(?:u(?:r(?:s(?:e?)?)?)?)?)?)?)";
8894
+ const force = "-(?:f(?:o(?:r(?:c(?:e?)?)?)?)?)";
8895
+ return new RegExp(
8896
+ `\\b${removeVerb}\\b[^;\\n|]*(?:${recurseg}|${force})\\b`,
8897
+ "i"
8898
+ );
8899
+ }
8497
8900
  function lastPipelineCommand(command) {
8498
8901
  const segments = command.split("|").map((part) => part.trim()).filter(Boolean);
8499
8902
  return segments[segments.length - 1] || command;
8500
8903
  }
8501
- function splitShellSegments(command) {
8904
+ function splitShellSegments(command, shell = "bash") {
8502
8905
  const segments = [];
8503
8906
  let current = "";
8504
8907
  let quote;
8505
8908
  for (let index = 0; index < command.length; index += 1) {
8506
8909
  const char = command[index];
8507
8910
  if (quote) {
8508
- if (quote === '"' && char === "\\" && index + 1 < command.length) {
8911
+ if (quote === '"' && char === (shell === "powershell" ? "`" : "\\") && index + 1 < command.length) {
8509
8912
  current += char;
8510
8913
  current += command[index + 1];
8511
8914
  index += 1;
@@ -8520,13 +8923,13 @@ function splitShellSegments(command) {
8520
8923
  current += char;
8521
8924
  continue;
8522
8925
  }
8523
- if (char === "\\" && index + 1 < command.length) {
8926
+ if (char === (shell === "powershell" ? "`" : "\\") && index + 1 < command.length) {
8524
8927
  current += char;
8525
8928
  current += command[index + 1];
8526
8929
  index += 1;
8527
8930
  continue;
8528
8931
  }
8529
- if (char === "&" || char === "|" || char === ";" || char === "\n") {
8932
+ if (char === "&" || char === "|" || char === ";" || char === "\n" || shell === "powershell" && /[(){}]/.test(char)) {
8530
8933
  if ((char === "&" || char === "|") && command[index + 1] === char) {
8531
8934
  index += 1;
8532
8935
  }
@@ -8539,7 +8942,7 @@ function splitShellSegments(command) {
8539
8942
  segments.push(current);
8540
8943
  return segments.map((part) => part.trim()).filter(Boolean);
8541
8944
  }
8542
- function splitShellWords(segment) {
8945
+ function splitShellWords(segment, shell = "bash") {
8543
8946
  const words = [];
8544
8947
  let current = "";
8545
8948
  let quote;
@@ -8548,7 +8951,7 @@ function splitShellWords(segment) {
8548
8951
  if (quote) {
8549
8952
  if (char === quote) {
8550
8953
  quote = void 0;
8551
- } else if (quote === '"' && char === "\\" && index + 1 < segment.length) {
8954
+ } else if (quote === '"' && char === (shell === "powershell" ? "`" : "\\") && index + 1 < segment.length) {
8552
8955
  current += segment[index + 1];
8553
8956
  index += 1;
8554
8957
  } else {
@@ -8560,7 +8963,7 @@ function splitShellWords(segment) {
8560
8963
  quote = char;
8561
8964
  continue;
8562
8965
  }
8563
- if (char === "\\") {
8966
+ if (char === (shell === "powershell" ? "`" : "\\")) {
8564
8967
  if (index + 1 >= segment.length) return void 0;
8565
8968
  current += segment[index + 1];
8566
8969
  index += 1;
@@ -8769,7 +9172,7 @@ function classifyShellCommand(command, shell = "bash", options) {
8769
9172
  const warnings = [];
8770
9173
  let commandClass = "unknown_shell";
8771
9174
  let destructive = false;
8772
- const destructivePatterns = shell === "powershell" ? [...POWERSHELL_DESTRUCTIVE_PATTERNS, ...DESTRUCTIVE_PATTERNS] : shell === "cmd" ? [...CMD_DESTRUCTIVE_PATTERNS, ...DESTRUCTIVE_PATTERNS] : DESTRUCTIVE_PATTERNS;
9175
+ const destructivePatterns = shell === "cmd" ? [...CMD_DESTRUCTIVE_PATTERNS, ...DESTRUCTIVE_PATTERNS] : DESTRUCTIVE_PATTERNS;
8773
9176
  const removeWarnings = recursiveRemoveWarnings(command);
8774
9177
  if (removeWarnings.length > 0) {
8775
9178
  if (shell === "bash" && isStaticallyScopedRecursiveForceRemove(command, options)) {
@@ -8786,6 +9189,37 @@ function classifyShellCommand(command, shell = "bash", options) {
8786
9189
  commandClass = "unknown_shell";
8787
9190
  destructive = true;
8788
9191
  }
9192
+ if (shell === "powershell") {
9193
+ const segments = splitShellSegments(command, shell);
9194
+ const simple = !/[$`#@{}()[\]=<>]/.test(command) && segments.every((segment) => {
9195
+ const words2 = splitShellWords(segment, shell);
9196
+ if (!words2?.length) return false;
9197
+ const head = words2[0] === "." ? words2[1] : words2[0];
9198
+ 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);
9199
+ });
9200
+ if (!simple) {
9201
+ for (const entry of POWERSHELL_DESTRUCTIVE_PATTERNS) {
9202
+ if (!entry.pattern.test(command)) continue;
9203
+ warnings.push(entry.warning);
9204
+ commandClass = entry.commandClass;
9205
+ destructive = true;
9206
+ }
9207
+ }
9208
+ for (const segment of segments) {
9209
+ const words2 = splitShellWords(segment, shell) ?? segment.split(/\s+/);
9210
+ if (words2[0] === ".") words2.shift();
9211
+ if (!words2[0]) continue;
9212
+ words2[0] = executableName(words2[0]);
9213
+ for (const entry of POWERSHELL_DESTRUCTIVE_PATTERNS) {
9214
+ const pattern = new RegExp(`^(?:${entry.pattern.source})`, entry.pattern.flags);
9215
+ if (pattern.test(words2.join(" "))) {
9216
+ warnings.push(entry.warning);
9217
+ commandClass = entry.commandClass;
9218
+ destructive = true;
9219
+ }
9220
+ }
9221
+ }
9222
+ }
8789
9223
  for (const entry of destructivePatterns) {
8790
9224
  if (entry.pattern.test(command)) {
8791
9225
  warnings.push(entry.warning);
@@ -8801,8 +9235,6 @@ function classifyShellCommand(command, shell = "bash", options) {
8801
9235
  const primary = words[0] || firstWord(normalized);
8802
9236
  if (shell === "powershell" && /\b(invoke-webrequest|iwr|invoke-restmethod|irm)\b/.test(normalized)) {
8803
9237
  commandClass = "network";
8804
- } else if (shell === "powershell" && /\b(new-item|set-content|add-content|copy-item|move-item|rename-item)\b/.test(normalized)) {
8805
- commandClass = "filesystem_write";
8806
9238
  } else if (shell === "cmd" && /\b(curl|bitsadmin|certutil)\b/.test(normalized)) {
8807
9239
  commandClass = "network";
8808
9240
  } else if (shell === "cmd" && /\b(copy|xcopy|robocopy|move|mkdir|md|ren|rename)\b/.test(normalized)) {
@@ -9104,7 +9536,7 @@ function createAgentInstance(deps, systemPrompt, tools, model, apiKey, thinkingL
9104
9536
  hooks.observation.lastWorkAt = Date.now();
9105
9537
  hooks.observation.lastActivity = "response_event";
9106
9538
  }
9107
- }, hooks);
9539
+ }, hooks, deps.recovery);
9108
9540
  if (deps.createAgent) {
9109
9541
  const agent2 = deps.createAgent({
9110
9542
  streamFn,
@@ -9578,10 +10010,21 @@ ${DELEGATION_REPORT_INSTRUCTION}`;
9578
10010
  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.`);
9579
10011
  }
9580
10012
  const detail = terminalError ? `: ${terminalError.slice(0, 200)}` : "";
9581
- 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}`);
10013
+ const hooks2 = agentRunHooks.get(subagent);
10014
+ const fallbackFailure = lastAssistant && (lastAssistant.retryable !== void 0 || lastAssistant.replayClass !== void 0 || lastAssistant.replayEligible !== void 0 || lastAssistant.remoteFailure !== void 0) ? {
10015
+ errorMessage: terminalError,
10016
+ retryable: lastAssistant.retryable,
10017
+ replayClass: lastAssistant.replayClass,
10018
+ replayEligible: lastAssistant.replayEligible,
10019
+ remoteFailure: lastAssistant.remoteFailure
10020
+ } : void 0;
10021
+ const subagentFailure = hooks2?.failure ?? fallbackFailure;
10022
+ const overflow = subagentFailure?.recovery?.outcome === "overflow";
10023
+ 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}`);
9582
10024
  const carrier = failure;
9583
- carrier.retryable = lastAssistant?.retryable;
9584
- carrier.replayClass = lastAssistant?.replayClass;
10025
+ carrier.retryable = subagentFailure?.retryable;
10026
+ carrier.replayClass = subagentFailure?.replayClass;
10027
+ carrier.subagentFailure = subagentFailure;
9585
10028
  throw failure;
9586
10029
  }
9587
10030
  let response;
@@ -9628,8 +10071,15 @@ ${DELEGATION_REPORT_INSTRUCTION}`;
9628
10071
  phaseProgress(task.observation.phase);
9629
10072
  task.error = errorMessage.slice(0, 2e3);
9630
10073
  const failureCarrier = error;
9631
- if (failureCarrier && (failureCarrier.retryable !== void 0 || failureCarrier.replayClass !== void 0)) {
9632
- task.failure = { ...failureCarrier.retryable !== void 0 ? { retryable: failureCarrier.retryable } : {}, ...failureCarrier.replayClass !== void 0 ? { replayClass: failureCarrier.replayClass } : {} };
10074
+ if (failureCarrier?.subagentFailure) {
10075
+ task.failure = failureCarrier.subagentFailure;
10076
+ } else if (failureCarrier && (failureCarrier.retryable !== void 0 || failureCarrier.replayClass !== void 0 || failureCarrier.replayEligible !== void 0 || failureCarrier.remoteFailure !== void 0)) {
10077
+ task.failure = {
10078
+ ...failureCarrier.retryable !== void 0 ? { retryable: failureCarrier.retryable } : {},
10079
+ ...failureCarrier.replayClass !== void 0 ? { replayClass: failureCarrier.replayClass } : {},
10080
+ ...failureCarrier.replayEligible !== void 0 ? { replayEligible: failureCarrier.replayEligible } : {},
10081
+ ...failureCarrier.remoteFailure !== void 0 ? { remoteFailure: failureCarrier.remoteFailure } : {}
10082
+ };
9633
10083
  } else {
9634
10084
  task.failure = void 0;
9635
10085
  }
@@ -9714,6 +10164,11 @@ function buildParallelResult(tasks, totalDuration) {
9714
10164
  `;
9715
10165
  if (task.observation) resultText += `Tools: ${task.observation.toolStarts} started, ${task.observation.toolEnds} ended, ${task.observation.toolErrors} errors
9716
10166
  `;
10167
+ if (task.failure?.recovery) {
10168
+ const recovery = task.failure.recovery;
10169
+ 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}` : ""}
10170
+ `;
10171
+ }
9717
10172
  if (task.status === "completed" && task.result) {
9718
10173
  resultText += `${task.result}
9719
10174
  `;
@@ -9965,40 +10420,8 @@ ${SPAWN_USAGE_HINT}` }],
9965
10420
  toolContext.log?.(`Spawning ${tasks.length} parallel subagents: ${tasks.map((task) => task.name).join(", ")}`);
9966
10421
  toolContext.reportProgress?.({ message: `Launching ${tasks.length} parallel subagents...`, increment: 5 });
9967
10422
  const startTime = Date.now();
9968
- const runTask = async (task) => {
9969
- await run(task, toolContext.reportProgress, signal);
9970
- if (task.wasRecall || task.mode !== "read" || task.status !== "failed" || !task.error) return;
9971
- if (signal?.aborted) return;
9972
- if (!isRetryableSubagentFailureFrom({ errorMessage: task.error, retryable: task.failure?.retryable, replayClass: task.failure?.replayClass })) return;
9973
- observeDelegationPhase(task.observation, "task_replay");
9974
- const settleMs = Math.max(0, (deps.transientRetryBackoffMs ?? 2e3) * 10);
9975
- toolContext.reportProgress?.({ message: `[${task.name}] Provider/transport failure \u2014 replaying the read task once after ${Math.round(settleMs / 1e3)}s`, increment: 0 });
9976
- await cancellableBackoff(settleMs, signal);
9977
- if (signal.aborted) {
9978
- task.status = "cancelled";
9979
- task.error = "Subagent cancelled during replay backoff";
9980
- task.failure = void 0;
9981
- observeDelegationPhase(task.observation, "cancelled");
9982
- task.completedAt = Date.now();
9983
- return;
9984
- }
9985
- task.evidence = [];
9986
- task.confidenceSignals = [];
9987
- task.runtimeEvidence = [];
9988
- task.reportedEvidence = [];
9989
- task.error = void 0;
9990
- task.failure = void 0;
9991
- task.report = void 0;
9992
- task.result = void 0;
9993
- task.details = void 0;
9994
- task.parentHandoffRequired = false;
9995
- task.sessionId = void 0;
9996
- task.requestedMode = void 0;
9997
- task.wasRecall = void 0;
9998
- await run(task, toolContext.reportProgress, signal);
9999
- };
10000
10423
  const batch = Promise.resolve().then(() => runWithConcurrencyLimit(
10001
- tasks.map((task) => () => runTask(task)),
10424
+ tasks.map((task) => () => run(task, toolContext.reportProgress, signal)),
10002
10425
  Math.max(1, deps.maxConcurrentSubagents ?? DEFAULT_MAX_CONCURRENT_SUBAGENTS)
10003
10426
  ));
10004
10427
  pending.add(batch);
@@ -10041,9 +10464,8 @@ ${SPAWN_USAGE_HINT}` }],
10041
10464
  observation: task.observation,
10042
10465
  parentHandoffRequired: task.parentHandoffRequired,
10043
10466
  failureReason: task.error,
10044
- // Structured provider verdict so the parent model can distinguish
10045
- // stream loss (safe to re-dispatch; no tool calls executed) from hard
10046
- // provider vetoes (auth/quota/config — not recoverable here).
10467
+ // Structured provider facts plus the actual recovery outcome. Failed-
10468
+ // response tool calls never execute; do not infer their absence.
10047
10469
  failure: task.failure,
10048
10470
  details: task.details
10049
10471
  })),
@@ -10266,7 +10688,11 @@ function executeManageTodoList(args, currentTodos) {
10266
10688
  };
10267
10689
  }
10268
10690
  default:
10269
- return rejectTodoAction("read", `unknown action: ${args.action}`, safeCurrentTodos);
10691
+ return rejectTodoAction(
10692
+ "read",
10693
+ `unknown action: ${args.action}. Valid actions: create, read, complete_current, clear.`,
10694
+ safeCurrentTodos
10695
+ );
10270
10696
  }
10271
10697
  }
10272
10698
 
@@ -11115,6 +11541,7 @@ var resolveToCwd = resolveInSandbox;
11115
11541
  var resolveReadPath = resolveInSandbox;
11116
11542
 
11117
11543
  // ../../packages/shared-headless-capabilities/src/builtin/fs/schema-utils.ts
11544
+ 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.";
11118
11545
  function formatPrefixes(prefixes) {
11119
11546
  if (!prefixes || prefixes.length === 0) {
11120
11547
  return "";
@@ -11124,6 +11551,11 @@ function formatPrefixes(prefixes) {
11124
11551
  return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
11125
11552
  }).join(", ");
11126
11553
  }
11554
+ function arraysEqual(a, b) {
11555
+ if (a === b) return true;
11556
+ if (a.length !== b.length) return false;
11557
+ return a.every((value, index) => value === b[index]);
11558
+ }
11127
11559
  function buildRootParameterSchema(sandboxRoot, options) {
11128
11560
  const roots = getAvailableRoots(options);
11129
11561
  if (roots.length === 0) {
@@ -11147,8 +11579,11 @@ function buildRootParameterSchema(sandboxRoot, options) {
11147
11579
  function buildPathResolutionDescription(sandboxRoot, options) {
11148
11580
  const roots = getAvailableRoots(options);
11149
11581
  const allowlistedDirs = options?.allowlistedDirs ?? [];
11150
- const hasGlobalAllowlist = allowlistedDirs.some((dir) => dir.trim() === "/");
11151
- 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(", ")}.` : "";
11582
+ const descriptionOverride = options?.descriptionAllowlistedDirs;
11583
+ const describedAllowlist = descriptionOverride ?? allowlistedDirs;
11584
+ const viewsEqual = arraysEqual(describedAllowlist, allowlistedDirs);
11585
+ const hasGlobalAllowlist = describedAllowlist.some((dir) => dir.trim() === "/") && viewsEqual;
11586
+ 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;
11152
11587
  if (roots.length === 0) {
11153
11588
  return ["Relative paths resolve against the workspace root.", allowlistSentence].filter(Boolean).join(" ");
11154
11589
  }
@@ -11673,9 +12108,10 @@ Focus: ${question.trim()}` : ""}` },
11673
12108
  return;
11674
12109
  }
11675
12110
  if (magicInfo?.kind === "image") {
12111
+ const inlineSupported = INLINE_IMAGE_MIME_TYPES.has(magicInfo.mimeType);
11676
12112
  resolve2({
11677
- content: [{ type: "text", text: `Cannot attach image file ${path3}: unsupported image payload ${magicInfo.mimeType}.` }],
11678
- details: { understanding: { kind: "image", mimeType: magicInfo.mimeType, fallbackReason: "unsupported inline image format" } },
12113
+ 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}.` }],
12114
+ details: { understanding: { kind: "image", mimeType: magicInfo.mimeType, fallbackReason: inlineSupported ? "image bridge read failed" : "unsupported inline image format" } },
11679
12115
  isError: true
11680
12116
  });
11681
12117
  return;
@@ -12082,11 +12518,10 @@ function detectTextOnlyBinary(input, filePath, prefixMayBeTruncated) {
12082
12518
  async function readImageInfo(path3, fs2) {
12083
12519
  try {
12084
12520
  let base64 = "";
12085
- const size = (await fs2.stat(path3).catch(() => void 0))?.size ?? 0;
12086
12521
  if (typeof fs2.readFile === "function") {
12087
12522
  base64 = await fs2.readFile(path3, "base64").catch(() => "");
12088
12523
  }
12089
- if (!base64 && size > 0 && typeof fs2.readFileSync === "function") {
12524
+ if (!base64 && typeof fs2.readFileSync === "function") {
12090
12525
  base64 = fs2.readFileSync(path3, "base64");
12091
12526
  }
12092
12527
  if (!base64) {
@@ -12114,6 +12549,8 @@ async function readImageInfo(path3, fs2) {
12114
12549
  if (bytes.length >= 12 && asciiAt(bytes, 0, 4) === "RIFF" && asciiAt(bytes, 8, 12 - 8) === "WEBP") {
12115
12550
  return { mimeType: "image/webp", base64, ...parseWebpDimensions(bytes) };
12116
12551
  }
12552
+ console.error(`[pie] readImageInfo decoded ${bytes.length} bytes for ${path3} but no known image signature matched`);
12553
+ return void 0;
12117
12554
  } catch (error) {
12118
12555
  console.error(`[pie] readImageInfo failed for ${path3}: ${error instanceof Error ? error.message : String(error)}`);
12119
12556
  return void 0;
@@ -12121,7 +12558,7 @@ async function readImageInfo(path3, fs2) {
12121
12558
  return void 0;
12122
12559
  }
12123
12560
  function decodeBase64ToBytes(base64) {
12124
- if (typeof Buffer !== "undefined") {
12561
+ if (hasUsableNodeBuffer()) {
12125
12562
  return Buffer.from(base64, "base64");
12126
12563
  }
12127
12564
  const clean = base64.replace(/[^A-Za-z0-9+/=]/g, "");
@@ -12140,6 +12577,17 @@ function decodeBase64ToBytes(base64) {
12140
12577
  }
12141
12578
  return bytes.subarray(0, at);
12142
12579
  }
12580
+ function hasUsableNodeBuffer() {
12581
+ if (typeof Buffer === "undefined" || typeof Buffer.from !== "function") {
12582
+ return false;
12583
+ }
12584
+ try {
12585
+ const probe = Buffer.from("aGk=", "base64");
12586
+ return probe.length === 2 && probe[0] === 104;
12587
+ } catch {
12588
+ return false;
12589
+ }
12590
+ }
12143
12591
  function readUIntBE(bytes, offset, length) {
12144
12592
  let value = 0;
12145
12593
  for (let i = 0; i < length; i += 1) {