@cydm/pie 2.7.11 → 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-XVF6V6Q5.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-3VEIP2VY.js";
15
+ } from "./chunk-EN47TIBG.js";
16
16
  import {
17
17
  Type
18
18
  } from "./chunk-XXGYUYXI.js";
@@ -1003,7 +1003,7 @@ 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
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) {
@@ -1331,7 +1331,9 @@ var DEFAULT_COMPACTION_CONFIG = {
1331
1331
  summaryPrefix: SUMMARY_PREFIX
1332
1332
  };
1333
1333
  var APPROX_BYTES_PER_TOKEN = 4;
1334
- 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;
1335
1337
  function estimateTokens(message) {
1336
1338
  return approximateBytesAsTokens(estimateModelVisibleBytes(message));
1337
1339
  }
@@ -1456,20 +1458,16 @@ function planRetainedTail(summaries, options = {}) {
1456
1458
  if (boundaries.length === 0) {
1457
1459
  return nonePlan("no-boundary");
1458
1460
  }
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
1461
  const groupEnd = boundaries.map((_, index) => boundaries[index + 1] ?? summaries.length);
1464
1462
  const planFrom = (startIndex, reason) => ({
1465
1463
  retainedTail: { kind: "from-entry", entryId: summaries[startIndex].id },
1466
1464
  entryIds: summaries.slice(startIndex).map((summary) => summary.id),
1467
- tailTokens: prefix[summaries.length] - prefix[startIndex],
1465
+ tailTokens: estimateRetainedTailTokens(summaries, { kind: "from-entry", entryId: summaries[startIndex].id }),
1468
1466
  reason
1469
1467
  });
1470
1468
  let accumulated = 0;
1471
1469
  for (let k = boundaries.length - 1; k >= 0; k--) {
1472
- 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);
1473
1471
  if (groupTokens > budget) {
1474
1472
  if (accumulated === 0) {
1475
1473
  return nonePlan("single-group-over-budget");
@@ -1545,6 +1543,9 @@ function estimateCompactionGain(params) {
1545
1543
  const tokensBefore = Math.max(0, params.tokensBefore);
1546
1544
  const tokenSavings = Math.max(0, tokensBefore - projectedTokens);
1547
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
+ }
1548
1549
  if (params.budgetTokens !== void 0 && params.budgetTokens > 0 && projectedTokens > params.budgetTokens) {
1549
1550
  return {
1550
1551
  ...estimate,
@@ -1825,7 +1826,7 @@ function normalizeUserContentBlock(block) {
1825
1826
  return { type: "text", text: block.text };
1826
1827
  }
1827
1828
  if (block.type === "hostContext") {
1828
- return { type: "text", text: "x".repeat(512) };
1829
+ return { type: "hostContext" };
1829
1830
  }
1830
1831
  if (block.type === "image") {
1831
1832
  return { type: "image", mimeType: block.mimeType };
@@ -1857,22 +1858,16 @@ function estimateMessageMediaBytes(message) {
1857
1858
  }
1858
1859
  function estimateUserContentBlockMediaBytes(block) {
1859
1860
  if (block.type === "image") {
1860
- return estimateImageContentBytes(block);
1861
+ return IMAGE_MEDIA_BYTES_ESTIMATE;
1861
1862
  }
1862
1863
  if (block.type === "audio") {
1863
- return estimateInlineDataBytes(block);
1864
+ return AUDIO_MEDIA_BYTES_ESTIMATE;
1864
1865
  }
1865
1866
  if (block.type === "video") {
1866
- return estimateInlineDataBytes(block);
1867
+ return VIDEO_MEDIA_BYTES_ESTIMATE;
1867
1868
  }
1868
1869
  return 0;
1869
1870
  }
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
1871
  function approximateBytesAsTokens(bytes) {
1877
1872
  return Math.ceil(bytes / APPROX_BYTES_PER_TOKEN);
1878
1873
  }
@@ -4623,10 +4618,26 @@ function prepareSessionCompaction(params) {
4623
4618
  const agentOnlyFailure = excludedFailure === null && requireExclusion ? agentTail ?? null : null;
4624
4619
  const summaries = sessionManager.getActiveContextSummaries();
4625
4620
  const entries = sessionManager.getResidentContextEntries();
4626
- const effectiveSummaries = excludedFailure ? summaries.filter((summary) => summary.id !== excludedFailure.entryId) : summaries;
4621
+ const sourceSummaries = excludedFailure ? summaries.filter((summary) => summary.id !== excludedFailure.entryId) : summaries;
4627
4622
  const effectiveEntries = excludedFailure ? entries.filter((entry) => entry.id !== excludedFailure.entryId) : entries;
4628
4623
  const summaryInput = excludedFailure || agentOnlyFailure ? messages.slice(0, -1) : messages;
4629
- 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);
4630
4641
  const reductionLimit = inputTokens >= 1e3 ? inputTokens - Math.ceil(inputTokens * 0.1) : Infinity;
4631
4642
  const contextLimit = Math.min(
4632
4643
  reductionLimit,
@@ -4640,7 +4651,7 @@ function prepareSessionCompaction(params) {
4640
4651
  let planned = planRetainedTail(effectiveSummaries, { ...params.compactionConfig, keepRecentTokens: tailBudget });
4641
4652
  if (planned.retainedTail.kind !== "none") {
4642
4653
  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" };
4654
+ planned = validation.ok ? { ...planned, entryIds: validation.entryIds, tailTokens: estimateRetainedTailTokens(effectiveSummaries, planned.retainedTail) } : { retainedTail: { kind: "none" }, entryIds: [], tailTokens: 0, reason: "pairing" };
4644
4655
  }
4645
4656
  return {
4646
4657
  sessionId: sessionManager.getActiveSessionId(),
@@ -4696,7 +4707,7 @@ function verifyPreparedCompactionCommit(params) {
4696
4707
  const gain = estimateCompactionGain({
4697
4708
  summary: checkpointSummary,
4698
4709
  tailTokens: estimateRetainedTailTokens(params.preparation.summaries, retainedTail),
4699
- tokensBefore: params.result.tokensBefore ?? params.preparation.inputTokens,
4710
+ tokensBefore: params.preparation.inputTokens,
4700
4711
  budgetTokens: params.preparation.budgetTokens
4701
4712
  });
4702
4713
  if (!gain.ok) {
@@ -5176,7 +5187,13 @@ async function preparePromptMessages(params) {
5176
5187
 
5177
5188
  // ../../packages/agent-framework/src/session/controller/pre-request-policy.ts
5178
5189
  async function runPreRequestAutoCompact(params) {
5179
- 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) {
5180
5197
  return;
5181
5198
  }
5182
5199
  const decision = params.getAutoCompactDecision?.({
@@ -5185,12 +5202,13 @@ async function runPreRequestAutoCompact(params) {
5185
5202
  cwd: params.cwd,
5186
5203
  messages: params.agent.state.messages.slice()
5187
5204
  });
5188
- if (decision?.reason !== "threshold") {
5205
+ if (cancelled() || decision?.reason !== "threshold") {
5189
5206
  return;
5190
5207
  }
5191
5208
  try {
5192
5209
  await params.runAutoCompact();
5193
- } catch {
5210
+ } catch (error) {
5211
+ if (boundary) throw error;
5194
5212
  if (params.isCurrent?.() !== false) params.setRecoveryState("none");
5195
5213
  }
5196
5214
  }
@@ -5444,12 +5462,7 @@ var AgentSessionQueueDispatcher = class {
5444
5462
  return false;
5445
5463
  }
5446
5464
  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 {
5465
+ if (!dispatched.deliveredToAgent) {
5453
5466
  const prepared = await this.options.preparePromptMessages(dispatched.messages, {
5454
5467
  source: dispatched.source,
5455
5468
  streamingBehavior: dispatched.mode,
@@ -5459,13 +5472,20 @@ var AgentSessionQueueDispatcher = class {
5459
5472
  visibility: dispatched.visibility
5460
5473
  });
5461
5474
  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();
5475
+ if (prepared.handled) return true;
5476
+ if (prepared.systemPrompt !== void 0) {
5477
+ this.options.agent.setSystemPrompt(prepared.systemPrompt);
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);
5468
5484
  }
5485
+ await this.options.continueTurn();
5486
+ } else {
5487
+ await this.options.agent.prompt(dispatched.messages);
5488
+ await this.options.syncNewAgentMessages();
5469
5489
  }
5470
5490
  return true;
5471
5491
  }
@@ -5586,6 +5606,95 @@ var ReplayStallCircuit = class {
5586
5606
  }
5587
5607
  };
5588
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
+
5589
5698
  // ../../packages/agent-framework/src/session/controller/retry-policy.ts
5590
5699
  function defaultSetTimeout4(callback, delay) {
5591
5700
  return setTimeout(callback, delay);
@@ -5600,21 +5709,14 @@ var AgentSessionRetryController = class {
5600
5709
  this.removeTrailingAssistantError = removeTrailingAssistantError2;
5601
5710
  this.continueTurn = continueTurn;
5602
5711
  this.setRecoveryState = setRecoveryState;
5603
- this.replayCircuit = new ReplayStallCircuit(retry?.systemicReplayStallLimit);
5712
+ this.episode = new AgentSessionRetryEpisode(retry);
5604
5713
  }
5605
5714
  retry;
5606
5715
  emit;
5607
5716
  removeTrailingAssistantError;
5608
5717
  continueTurn;
5609
5718
  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;
5719
+ episode;
5618
5720
  retryTimer;
5619
5721
  retryEpoch = 0;
5620
5722
  pendingRetry;
@@ -5628,72 +5730,45 @@ var AgentSessionRetryController = class {
5628
5730
  waitForRetry() {
5629
5731
  return this.pendingRetry ?? Promise.resolve();
5630
5732
  }
5631
- /** L2 circuit: healthy turn (success or an actively-answered failure). */
5632
- noteEndpointHealthy() {
5633
- this.replayCircuit.recordHealthyTurn();
5634
- }
5635
5733
  async handleTurnEnd(message, failure) {
5636
5734
  if (!this.retry || !message || message.role !== "assistant") {
5637
5735
  this.settleStrandedRetry(failure);
5638
5736
  return;
5639
5737
  }
5640
5738
  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();
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 });
5646
5743
  this.setRecoveryState("none");
5647
5744
  this.resolvePending();
5648
5745
  }
5649
5746
  return;
5650
5747
  }
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();
5748
+ const decision = this.episode.next(failure);
5749
+ if (decision.action === "terminal") {
5750
+ if (decision.totalRetryAttempts > 0) {
5657
5751
  this.setRecoveryState("none");
5658
5752
  this.resolvePending();
5659
5753
  }
5660
5754
  return;
5661
5755
  }
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
- }
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
+ });
5687
5763
  }
5688
- this.emit({ type: "retry_exhausted", errorMessage: terminalFailure.message, maxRetries: totalExhausted ? totalBudget : maxRetries });
5689
- 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 });
5690
5766
  this.emit({ type: "turn_settled" });
5691
- this.resetAttempts();
5692
5767
  this.setRecoveryState("terminal");
5693
5768
  this.resolvePending();
5694
5769
  return;
5695
5770
  }
5696
- const attempt = kindAttempts;
5771
+ const { attempt, maxRetries, delayMs } = decision;
5697
5772
  this.ensurePending();
5698
5773
  const epoch = this.retryEpoch;
5699
5774
  const pending = this.pendingRetry;
@@ -5720,7 +5795,7 @@ var AgentSessionRetryController = class {
5720
5795
  this.emit({ type: "retry_exhausted", errorMessage, maxRetries });
5721
5796
  this.emit({ type: "turn_failed_terminal", failure: terminalFailure, message });
5722
5797
  this.emit({ type: "turn_settled" });
5723
- this.resetAttempts();
5798
+ this.episode.resetAttempts();
5724
5799
  this.setRecoveryState("terminal");
5725
5800
  this.resolvePending(pending);
5726
5801
  });
@@ -5745,24 +5820,20 @@ var AgentSessionRetryController = class {
5745
5820
  userAbort: false,
5746
5821
  terminal: true
5747
5822
  });
5748
- 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);
5749
5824
  this.emit({ type: "retry_exhausted", errorMessage: terminalFailure.message, maxRetries });
5750
5825
  this.emit({ type: "turn_failed_terminal", failure: terminalFailure });
5751
5826
  this.emit({ type: "turn_settled" });
5752
- this.resetAttempts();
5827
+ this.episode.resetAttempts();
5753
5828
  this.setRecoveryState("terminal");
5754
5829
  this.resolvePending();
5755
5830
  }
5756
5831
  cancel() {
5757
5832
  this.retryEpoch++;
5758
5833
  this.cancelTimerOnly();
5759
- this.resetAttempts();
5834
+ this.episode.resetAttempts();
5760
5835
  this.resolvePending();
5761
5836
  }
5762
- resetAttempts() {
5763
- this.retryAttempts.clear();
5764
- this.totalRetryAttempts = 0;
5765
- }
5766
5837
  cancelTimerOnly() {
5767
5838
  if (this.retryTimer === void 0) {
5768
5839
  return;
@@ -6101,7 +6172,8 @@ async function runAgentSessionCompaction(params) {
6101
6172
  const preparation = prepareSessionCompaction({
6102
6173
  sessionManager: params.sessionManager,
6103
6174
  messages: params.agent.state.messages,
6104
- budgetTokens: params.compactionBudgetTokens?.()
6175
+ budgetTokens: params.compactionBudgetTokens?.(),
6176
+ modelView: params.compaction.modelView
6105
6177
  });
6106
6178
  let result = await params.compaction.compact({
6107
6179
  reason: params.reason,
@@ -6380,6 +6452,8 @@ var AgentSessionController = class {
6380
6452
  autoContinue;
6381
6453
  unsubscribeAgent;
6382
6454
  unsubscribeSemantic;
6455
+ unsubscribeToolContinuation;
6456
+ compactionOwner;
6383
6457
  lastSyncedMessageCount = 0;
6384
6458
  disposed = false;
6385
6459
  transitioning = false;
@@ -6483,6 +6557,7 @@ var AgentSessionController = class {
6483
6557
  });
6484
6558
  this.syncFromActiveSession();
6485
6559
  this.unsubscribeAgent = this.agent.subscribe((event) => this.handleAgentEvent(event));
6560
+ this.unsubscribeToolContinuation = this.agent.setBeforeToolContinuation((signal) => this.runPreRequestAutoCompactIfNeeded(signal));
6486
6561
  this.unsubscribeSemantic = this.agent.subscribeSemantic((event) => {
6487
6562
  this.emit({ type: "semantic_event", event });
6488
6563
  });
@@ -6533,6 +6608,7 @@ var AgentSessionController = class {
6533
6608
  this.transitioning = true;
6534
6609
  try {
6535
6610
  this.generation++;
6611
+ if (this.isStreaming) this.agent.resetProcessingState();
6536
6612
  this.cancelScheduledWork();
6537
6613
  this.recoveryState = "none";
6538
6614
  this.pendingCompactionFailure = void 0;
@@ -6664,48 +6740,61 @@ var AgentSessionController = class {
6664
6740
  const generation = this.generation;
6665
6741
  if (this.disposed || this.transitioning) throw new Error("Controller disposed or transitioning");
6666
6742
  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
- }));
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
+ }
6699
6786
  }
6700
6787
  dispose() {
6701
6788
  if (this.disposed) return;
6702
6789
  this.disposed = true;
6790
+ if (this.isStreaming) this.agent.resetProcessingState();
6703
6791
  this.agent.releaseTransport?.();
6704
6792
  this.generation++;
6705
6793
  this.pendingEndOperations.clear();
6706
6794
  this.cancelScheduledWork();
6707
6795
  this.unsubscribeAgent?.();
6708
6796
  this.unsubscribeSemantic?.();
6797
+ this.unsubscribeToolContinuation?.();
6709
6798
  this.queue.clear();
6710
6799
  this.convergence.invalidate("dispose");
6711
6800
  this.listeners.clear();
@@ -6729,19 +6818,26 @@ var AgentSessionController = class {
6729
6818
  });
6730
6819
  }
6731
6820
  }
6732
- async runPreRequestAutoCompactIfNeeded() {
6821
+ async runPreRequestAutoCompactIfNeeded(signal) {
6733
6822
  const generation = this.generation;
6734
6823
  return runPreRequestAutoCompact({
6735
6824
  isCurrent: () => generation === this.generation && !this.disposed,
6736
6825
  disposed: this.disposed,
6737
6826
  runtimeGuardTriggered: this.runtimeGuard.triggered,
6738
6827
  isStreaming: this.isStreaming,
6739
- recoveryActive: this.isRecoveryActive(),
6828
+ recoveryActive: () => this.isRecoveryActive(),
6740
6829
  agent: this.agent,
6741
6830
  sessionManager: this.sessionManager,
6742
6831
  cwd: this.cwd,
6743
6832
  getAutoCompactDecision: this.compactionSettings.getAutoCompactDecision,
6744
- 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
+ },
6745
6841
  setRecoveryState: (state) => {
6746
6842
  this.recoveryState = state;
6747
6843
  }
@@ -6886,6 +6982,7 @@ var AgentSessionController = class {
6886
6982
  return this.recoveryState !== "none" || this.retryController.hasPendingRetry || this.autoCompact.hasPendingAutoCompact;
6887
6983
  }
6888
6984
  cancelScheduledWork() {
6985
+ this.compactionOwner = void 0;
6889
6986
  this.retryController.cancel();
6890
6987
  this.autoCompact.cancel();
6891
6988
  this.queueDispatcher.cancel();
@@ -8241,6 +8338,9 @@ function withSubagentRunContext(stream, hooks) {
8241
8338
  ${SUBAGENT_FINALIZATION_HINT}` } : context, options);
8242
8339
  };
8243
8340
  }
8341
+ function isRetryableSubagentFailure(message) {
8342
+ return isRetryableSubagentFailureFrom({ errorMessage: message });
8343
+ }
8244
8344
  function isRetryableSubagentFailureFrom(facts) {
8245
8345
  if (facts.retryable === false) return false;
8246
8346
  if (facts.replayClass === "stream_lost" || facts.replayClass === "stream_lost_partial") return true;
@@ -8262,90 +8362,366 @@ function cancellableBackoff(ms, signal) {
8262
8362
  if (signal?.aborted) finish();
8263
8363
  });
8264
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
+ }
8265
8375
  function factsOf(error) {
8266
8376
  if (error instanceof Error) {
8267
8377
  const carrier = error;
8268
- 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
+ };
8269
8386
  }
8270
- return { errorMessage: String(error) };
8387
+ return { errorMessage: String(error), stopReason: "error" };
8271
8388
  }
8272
- 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;
8273
8508
  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]();
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;
8287
8526
  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;
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
+ }
8307
8554
  }
8555
+ } finally {
8556
+ await closeIterator(iterator, hooks, options?.signal);
8308
8557
  }
8309
- } finally {
8310
- try {
8311
- await iterator.return?.();
8312
- } 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;
8313
8565
  }
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 {
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);
8323
8580
  }
8324
- await cancellableBackoff(backoffMs * (attempt + 1), options?.signal);
8325
- 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);
8326
8597
  }
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 {
8598
+ if (options?.signal?.aborted) {
8599
+ episode.resetAttempts();
8600
+ throw new Error("Subagent aborted by parent");
8334
8601
  }
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 });
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);
8343
8634
  }
8344
- result.end();
8345
- return result;
8635
+ } finally {
8636
+ episode.resetAttempts();
8346
8637
  }
8347
8638
  };
8348
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
+ }
8349
8725
 
8350
8726
  // ../../packages/shared-headless-capabilities/src/subagent-sessions.ts
8351
8727
  var SUBAGENT_SESSION_RECORD_VERSION = 1;
@@ -8525,14 +8901,14 @@ function lastPipelineCommand(command) {
8525
8901
  const segments = command.split("|").map((part) => part.trim()).filter(Boolean);
8526
8902
  return segments[segments.length - 1] || command;
8527
8903
  }
8528
- function splitShellSegments(command) {
8904
+ function splitShellSegments(command, shell = "bash") {
8529
8905
  const segments = [];
8530
8906
  let current = "";
8531
8907
  let quote;
8532
8908
  for (let index = 0; index < command.length; index += 1) {
8533
8909
  const char = command[index];
8534
8910
  if (quote) {
8535
- if (quote === '"' && char === "\\" && index + 1 < command.length) {
8911
+ if (quote === '"' && char === (shell === "powershell" ? "`" : "\\") && index + 1 < command.length) {
8536
8912
  current += char;
8537
8913
  current += command[index + 1];
8538
8914
  index += 1;
@@ -8547,13 +8923,13 @@ function splitShellSegments(command) {
8547
8923
  current += char;
8548
8924
  continue;
8549
8925
  }
8550
- if (char === "\\" && index + 1 < command.length) {
8926
+ if (char === (shell === "powershell" ? "`" : "\\") && index + 1 < command.length) {
8551
8927
  current += char;
8552
8928
  current += command[index + 1];
8553
8929
  index += 1;
8554
8930
  continue;
8555
8931
  }
8556
- if (char === "&" || char === "|" || char === ";" || char === "\n") {
8932
+ if (char === "&" || char === "|" || char === ";" || char === "\n" || shell === "powershell" && /[(){}]/.test(char)) {
8557
8933
  if ((char === "&" || char === "|") && command[index + 1] === char) {
8558
8934
  index += 1;
8559
8935
  }
@@ -8566,7 +8942,7 @@ function splitShellSegments(command) {
8566
8942
  segments.push(current);
8567
8943
  return segments.map((part) => part.trim()).filter(Boolean);
8568
8944
  }
8569
- function splitShellWords(segment) {
8945
+ function splitShellWords(segment, shell = "bash") {
8570
8946
  const words = [];
8571
8947
  let current = "";
8572
8948
  let quote;
@@ -8575,7 +8951,7 @@ function splitShellWords(segment) {
8575
8951
  if (quote) {
8576
8952
  if (char === quote) {
8577
8953
  quote = void 0;
8578
- } else if (quote === '"' && char === "\\" && index + 1 < segment.length) {
8954
+ } else if (quote === '"' && char === (shell === "powershell" ? "`" : "\\") && index + 1 < segment.length) {
8579
8955
  current += segment[index + 1];
8580
8956
  index += 1;
8581
8957
  } else {
@@ -8587,7 +8963,7 @@ function splitShellWords(segment) {
8587
8963
  quote = char;
8588
8964
  continue;
8589
8965
  }
8590
- if (char === "\\") {
8966
+ if (char === (shell === "powershell" ? "`" : "\\")) {
8591
8967
  if (index + 1 >= segment.length) return void 0;
8592
8968
  current += segment[index + 1];
8593
8969
  index += 1;
@@ -8796,7 +9172,7 @@ function classifyShellCommand(command, shell = "bash", options) {
8796
9172
  const warnings = [];
8797
9173
  let commandClass = "unknown_shell";
8798
9174
  let destructive = false;
8799
- 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;
8800
9176
  const removeWarnings = recursiveRemoveWarnings(command);
8801
9177
  if (removeWarnings.length > 0) {
8802
9178
  if (shell === "bash" && isStaticallyScopedRecursiveForceRemove(command, options)) {
@@ -8813,6 +9189,37 @@ function classifyShellCommand(command, shell = "bash", options) {
8813
9189
  commandClass = "unknown_shell";
8814
9190
  destructive = true;
8815
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
+ }
8816
9223
  for (const entry of destructivePatterns) {
8817
9224
  if (entry.pattern.test(command)) {
8818
9225
  warnings.push(entry.warning);
@@ -8828,8 +9235,6 @@ function classifyShellCommand(command, shell = "bash", options) {
8828
9235
  const primary = words[0] || firstWord(normalized);
8829
9236
  if (shell === "powershell" && /\b(invoke-webrequest|iwr|invoke-restmethod|irm)\b/.test(normalized)) {
8830
9237
  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
9238
  } else if (shell === "cmd" && /\b(curl|bitsadmin|certutil)\b/.test(normalized)) {
8834
9239
  commandClass = "network";
8835
9240
  } else if (shell === "cmd" && /\b(copy|xcopy|robocopy|move|mkdir|md|ren|rename)\b/.test(normalized)) {
@@ -9131,7 +9536,7 @@ function createAgentInstance(deps, systemPrompt, tools, model, apiKey, thinkingL
9131
9536
  hooks.observation.lastWorkAt = Date.now();
9132
9537
  hooks.observation.lastActivity = "response_event";
9133
9538
  }
9134
- }, hooks);
9539
+ }, hooks, deps.recovery);
9135
9540
  if (deps.createAgent) {
9136
9541
  const agent2 = deps.createAgent({
9137
9542
  streamFn,
@@ -9605,10 +10010,21 @@ ${DELEGATION_REPORT_INSTRUCTION}`;
9605
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.`);
9606
10011
  }
9607
10012
  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}`);
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}`);
9609
10024
  const carrier = failure;
9610
- carrier.retryable = lastAssistant?.retryable;
9611
- carrier.replayClass = lastAssistant?.replayClass;
10025
+ carrier.retryable = subagentFailure?.retryable;
10026
+ carrier.replayClass = subagentFailure?.replayClass;
10027
+ carrier.subagentFailure = subagentFailure;
9612
10028
  throw failure;
9613
10029
  }
9614
10030
  let response;
@@ -9655,8 +10071,15 @@ ${DELEGATION_REPORT_INSTRUCTION}`;
9655
10071
  phaseProgress(task.observation.phase);
9656
10072
  task.error = errorMessage.slice(0, 2e3);
9657
10073
  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 } : {} };
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
+ };
9660
10083
  } else {
9661
10084
  task.failure = void 0;
9662
10085
  }
@@ -9741,6 +10164,11 @@ function buildParallelResult(tasks, totalDuration) {
9741
10164
  `;
9742
10165
  if (task.observation) resultText += `Tools: ${task.observation.toolStarts} started, ${task.observation.toolEnds} ended, ${task.observation.toolErrors} errors
9743
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
+ }
9744
10172
  if (task.status === "completed" && task.result) {
9745
10173
  resultText += `${task.result}
9746
10174
  `;
@@ -9992,40 +10420,8 @@ ${SPAWN_USAGE_HINT}` }],
9992
10420
  toolContext.log?.(`Spawning ${tasks.length} parallel subagents: ${tasks.map((task) => task.name).join(", ")}`);
9993
10421
  toolContext.reportProgress?.({ message: `Launching ${tasks.length} parallel subagents...`, increment: 5 });
9994
10422
  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
10423
  const batch = Promise.resolve().then(() => runWithConcurrencyLimit(
10028
- tasks.map((task) => () => runTask(task)),
10424
+ tasks.map((task) => () => run(task, toolContext.reportProgress, signal)),
10029
10425
  Math.max(1, deps.maxConcurrentSubagents ?? DEFAULT_MAX_CONCURRENT_SUBAGENTS)
10030
10426
  ));
10031
10427
  pending.add(batch);
@@ -10068,9 +10464,8 @@ ${SPAWN_USAGE_HINT}` }],
10068
10464
  observation: task.observation,
10069
10465
  parentHandoffRequired: task.parentHandoffRequired,
10070
10466
  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).
10467
+ // Structured provider facts plus the actual recovery outcome. Failed-
10468
+ // response tool calls never execute; do not infer their absence.
10074
10469
  failure: task.failure,
10075
10470
  details: task.details
10076
10471
  })),
@@ -10293,7 +10688,11 @@ function executeManageTodoList(args, currentTodos) {
10293
10688
  };
10294
10689
  }
10295
10690
  default:
10296
- 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
+ );
10297
10696
  }
10298
10697
  }
10299
10698
 
@@ -11142,6 +11541,7 @@ var resolveToCwd = resolveInSandbox;
11142
11541
  var resolveReadPath = resolveInSandbox;
11143
11542
 
11144
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.";
11145
11545
  function formatPrefixes(prefixes) {
11146
11546
  if (!prefixes || prefixes.length === 0) {
11147
11547
  return "";
@@ -11151,6 +11551,11 @@ function formatPrefixes(prefixes) {
11151
11551
  return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
11152
11552
  }).join(", ");
11153
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
+ }
11154
11559
  function buildRootParameterSchema(sandboxRoot, options) {
11155
11560
  const roots = getAvailableRoots(options);
11156
11561
  if (roots.length === 0) {
@@ -11174,8 +11579,11 @@ function buildRootParameterSchema(sandboxRoot, options) {
11174
11579
  function buildPathResolutionDescription(sandboxRoot, options) {
11175
11580
  const roots = getAvailableRoots(options);
11176
11581
  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(", ")}.` : "";
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;
11179
11587
  if (roots.length === 0) {
11180
11588
  return ["Relative paths resolve against the workspace root.", allowlistSentence].filter(Boolean).join(" ");
11181
11589
  }
@@ -11700,9 +12108,10 @@ Focus: ${question.trim()}` : ""}` },
11700
12108
  return;
11701
12109
  }
11702
12110
  if (magicInfo?.kind === "image") {
12111
+ const inlineSupported = INLINE_IMAGE_MIME_TYPES.has(magicInfo.mimeType);
11703
12112
  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" } },
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" } },
11706
12115
  isError: true
11707
12116
  });
11708
12117
  return;
@@ -12109,11 +12518,10 @@ function detectTextOnlyBinary(input, filePath, prefixMayBeTruncated) {
12109
12518
  async function readImageInfo(path3, fs2) {
12110
12519
  try {
12111
12520
  let base64 = "";
12112
- const size = (await fs2.stat(path3).catch(() => void 0))?.size ?? 0;
12113
12521
  if (typeof fs2.readFile === "function") {
12114
12522
  base64 = await fs2.readFile(path3, "base64").catch(() => "");
12115
12523
  }
12116
- if (!base64 && size > 0 && typeof fs2.readFileSync === "function") {
12524
+ if (!base64 && typeof fs2.readFileSync === "function") {
12117
12525
  base64 = fs2.readFileSync(path3, "base64");
12118
12526
  }
12119
12527
  if (!base64) {
@@ -12141,6 +12549,8 @@ async function readImageInfo(path3, fs2) {
12141
12549
  if (bytes.length >= 12 && asciiAt(bytes, 0, 4) === "RIFF" && asciiAt(bytes, 8, 12 - 8) === "WEBP") {
12142
12550
  return { mimeType: "image/webp", base64, ...parseWebpDimensions(bytes) };
12143
12551
  }
12552
+ console.error(`[pie] readImageInfo decoded ${bytes.length} bytes for ${path3} but no known image signature matched`);
12553
+ return void 0;
12144
12554
  } catch (error) {
12145
12555
  console.error(`[pie] readImageInfo failed for ${path3}: ${error instanceof Error ? error.message : String(error)}`);
12146
12556
  return void 0;
@@ -12148,7 +12558,7 @@ async function readImageInfo(path3, fs2) {
12148
12558
  return void 0;
12149
12559
  }
12150
12560
  function decodeBase64ToBytes(base64) {
12151
- if (typeof Buffer !== "undefined") {
12561
+ if (hasUsableNodeBuffer()) {
12152
12562
  return Buffer.from(base64, "base64");
12153
12563
  }
12154
12564
  const clean = base64.replace(/[^A-Za-z0-9+/=]/g, "");
@@ -12167,6 +12577,17 @@ function decodeBase64ToBytes(base64) {
12167
12577
  }
12168
12578
  return bytes.subarray(0, at);
12169
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
+ }
12170
12591
  function readUIntBE(bytes, offset, length) {
12171
12592
  let value = 0;
12172
12593
  for (let i = 0; i < length; i += 1) {