@cydm/pie 2.6.4 → 2.6.6

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,8 +2,9 @@ import { createRequire as __createRequire } from "node:module"; const require =
2
2
  import {
3
3
  Agent,
4
4
  getToolCallerIdentity
5
- } from "./chunk-YEUBQBOB.js";
5
+ } from "./chunk-KEARZX6F.js";
6
6
  import {
7
+ AssistantMessageEventStream,
7
8
  FileSystemGateway,
8
9
  completeSimple,
9
10
  detectPlatform,
@@ -11,7 +12,7 @@ import {
11
12
  getPlatformConfig,
12
13
  hashText,
13
14
  streamSimple
14
- } from "./chunk-OCEYHWVX.js";
15
+ } from "./chunk-G5B6D35K.js";
15
16
  import {
16
17
  Type
17
18
  } from "./chunk-XXGYUYXI.js";
@@ -1070,6 +1071,73 @@ function createPlanCapability() {
1070
1071
  });
1071
1072
  }
1072
1073
 
1074
+ // ../../packages/shared-headless-capabilities/src/subagent-stream.ts
1075
+ function isRetryableSubagentFailure(message) {
1076
+ const remoteClosure = /SSE stream (?:aborted|closed before end|ended before response completed)/i.test(message);
1077
+ const classificationText = message.replace(/econnaborted/ig, "").replace(/SSE stream aborted/ig, "remote stream closed");
1078
+ if (/\b(?:400|401|403|404|422|429)\b|quota|weekly|monthly|limit reached|exhausted|unauthorized|authentication|invalid|protocol|stopReason=length|token limit|abort/i.test(classificationText)) return false;
1079
+ return remoteClosure || /Request timeout/i.test(message) || /econnreset|econnaborted|etimedout|eai_again|econnrefused|socket hang up|socket disconnected|secure TLS connection|fetch failed|network error|connection reset|premature close|\b50[0234]\b|bad gateway|service unavailable/i.test(message);
1080
+ }
1081
+ function cancellableBackoff(ms, signal) {
1082
+ return new Promise((resolve3) => {
1083
+ const finish = () => {
1084
+ clearTimeout(timer);
1085
+ signal?.removeEventListener("abort", finish);
1086
+ resolve3();
1087
+ };
1088
+ const timer = setTimeout(finish, ms);
1089
+ signal?.addEventListener("abort", finish, { once: true });
1090
+ if (signal?.aborted) finish();
1091
+ });
1092
+ }
1093
+ function withSubagentResponseRecovery(stream, backoffMs = 2e3) {
1094
+ return async (model, context, options) => {
1095
+ for (let attempt = 0; ; attempt++) {
1096
+ if (options?.signal?.aborted) throw new Error("Subagent aborted by parent");
1097
+ let message;
1098
+ try {
1099
+ const response = await stream(model, context, options);
1100
+ let terminal;
1101
+ const iterator = response[Symbol.asyncIterator]();
1102
+ try {
1103
+ while (true) {
1104
+ const next = await iterator.next();
1105
+ if (next.done) break;
1106
+ if (next.value.type === "done") terminal = next.value.message;
1107
+ if (next.value.type === "error") terminal = next.value.error;
1108
+ }
1109
+ } finally {
1110
+ try {
1111
+ await iterator.return?.();
1112
+ } catch {
1113
+ }
1114
+ }
1115
+ if (!terminal) throw new Error("Provider protocol error: response ended without a terminal event");
1116
+ message = terminal;
1117
+ } catch (error) {
1118
+ if (!options?.signal?.aborted && attempt < 2 && isRetryableSubagentFailure(error instanceof Error ? error.message : String(error))) {
1119
+ await cancellableBackoff(backoffMs * (attempt + 1), options?.signal);
1120
+ continue;
1121
+ }
1122
+ throw error;
1123
+ }
1124
+ if (options?.signal?.aborted) throw new Error("Subagent aborted by parent");
1125
+ if (message.stopReason === "error" && attempt < 2 && isRetryableSubagentFailure(message.errorMessage ?? "")) {
1126
+ await cancellableBackoff(backoffMs * (attempt + 1), options?.signal);
1127
+ continue;
1128
+ }
1129
+ const result = new AssistantMessageEventStream();
1130
+ if (message.stopReason === "error" || message.stopReason === "aborted") {
1131
+ result.push({ type: "error", reason: message.stopReason, error: { ...message, content: [] } });
1132
+ } else {
1133
+ result.push({ type: "done", reason: message.stopReason, message });
1134
+ }
1135
+ result.end();
1136
+ return result;
1137
+ }
1138
+ };
1139
+ }
1140
+
1073
1141
  // ../../packages/shared-headless-capabilities/src/bash-policy.ts
1074
1142
  var DESTRUCTIVE_PATTERNS = [
1075
1143
  { pattern: /\bgit\s+reset\s+--hard\b/, warning: "may discard uncommitted changes", commandClass: "git_destructive" },
@@ -1404,6 +1472,10 @@ function clipHeartbeatLabel(text, maxLength = 60) {
1404
1472
  function formatSkillsForPrompt(skills) {
1405
1473
  return formatSkillSummariesForPrompt(skills);
1406
1474
  }
1475
+ var SYNTHETIC_PROVIDER_TEXT_PREFIXES = ["[output truncated:", "[dropped "];
1476
+ function isSyntheticProviderText(text) {
1477
+ return SYNTHETIC_PROVIDER_TEXT_PREFIXES.some((prefix) => text.startsWith(prefix));
1478
+ }
1407
1479
  function collectLastAssistantText(messages) {
1408
1480
  for (let i = messages.length - 1; i >= 0; i--) {
1409
1481
  const message = messages[i];
@@ -1411,17 +1483,29 @@ function collectLastAssistantText(messages) {
1411
1483
  continue;
1412
1484
  }
1413
1485
  if (Array.isArray(message.content)) {
1414
- const textContent = message.content.filter((content) => content?.type === "text" && typeof content.text === "string").map((content) => content.text).join("\n");
1486
+ const realBlocks = message.content.filter((content) => content?.type === "text" && typeof content.text === "string").map((content) => content.text).filter((text) => !isSyntheticProviderText(text));
1487
+ const textContent = realBlocks.join("\n");
1415
1488
  if (textContent.trim()) {
1416
1489
  return textContent;
1417
1490
  }
1418
1491
  }
1419
- if (typeof message.content === "string" && message.content.trim()) {
1492
+ if (typeof message.content === "string" && message.content.trim() && !isSyntheticProviderText(message.content)) {
1420
1493
  return message.content;
1421
1494
  }
1422
1495
  }
1423
1496
  return "";
1424
1497
  }
1498
+ function visibleTextBlocks(message) {
1499
+ if (!Array.isArray(message.content)) return [];
1500
+ return message.content.filter((content) => content?.type === "text" && typeof content.text === "string").map((content) => content.text).filter((text) => text.trim().length > 0 && !isSyntheticProviderText(text));
1501
+ }
1502
+ function messageHasOwnText(message) {
1503
+ if (!message) return false;
1504
+ if (Array.isArray(message.content)) {
1505
+ return visibleTextBlocks(message).length > 0;
1506
+ }
1507
+ return typeof message.content === "string" && message.content.trim().length > 0 && !isSyntheticProviderText(message.content);
1508
+ }
1425
1509
  function inferComplexity(task, priority) {
1426
1510
  const normalized = task.toLowerCase();
1427
1511
  if (priority === "thorough" || task.length > 600 || /\b(across|architecture|refactor|review|investigate|debug|failing|test|compare|multiple|race|root cause)\b/.test(normalized)) {
@@ -1540,8 +1624,13 @@ function subagentOutputBudget(model) {
1540
1624
  return Math.min(model.maxTokens || 8192, 16384);
1541
1625
  }
1542
1626
  function createAgentInstance(deps, systemPrompt, tools, model, apiKey, thinkingLevel) {
1627
+ const streamFn = withSubagentResponseRecovery((streamModel, context, options) => streamSimple(streamModel, context, {
1628
+ ...options,
1629
+ maxTokens: options?.maxTokens ?? subagentOutputBudget(streamModel)
1630
+ }), deps.transientRetryBackoffMs);
1543
1631
  if (deps.createAgent) {
1544
1632
  return deps.createAgent({
1633
+ streamFn,
1545
1634
  systemPrompt,
1546
1635
  model,
1547
1636
  tools,
@@ -1560,10 +1649,7 @@ function createAgentInstance(deps, systemPrompt, tools, model, apiKey, thinkingL
1560
1649
  // Subagent turns are single-purpose report generations: request an
1561
1650
  // explicit output budget (see subagentOutputBudget) instead of leaving
1562
1651
  // it to endpoint defaults that silently truncate long reports.
1563
- streamFn: (streamModel, context, options) => streamSimple(streamModel, context, {
1564
- ...options,
1565
- maxTokens: options?.maxTokens ?? subagentOutputBudget(streamModel)
1566
- })
1652
+ streamFn
1567
1653
  });
1568
1654
  }
1569
1655
  function extractEvidenceFromToolArgs(toolName, args) {
@@ -1595,13 +1681,16 @@ function inferUncertainty(text, signals) {
1595
1681
  if (signals.some((signal) => signal.type === "tool_error" || signal.type === "model_fallback")) {
1596
1682
  return "medium";
1597
1683
  }
1684
+ if (signals.some((signal) => signal.type === "no_text_report")) {
1685
+ return "medium";
1686
+ }
1598
1687
  if (signals.some((signal) => signal.type === "tool_success" || signal.type === "file_read" || signal.type === "command_run")) {
1599
1688
  return "low";
1600
1689
  }
1601
1690
  return "medium";
1602
1691
  }
1603
1692
  function isParentHandoffRequired(task) {
1604
- return task.mode === "edit" || task.needsParentWrite || task.status === "timeout" || task.status === "failed" || task.confidenceSignals.some((signal) => signal.type === "tool_error" || signal.type === "no_tool_use");
1693
+ return task.mode === "edit" || task.needsParentWrite || task.status === "timeout" || task.status === "failed" || task.confidenceSignals.some((signal) => signal.type === "tool_error" || signal.type === "no_tool_use" || signal.type === "no_text_report");
1605
1694
  }
1606
1695
  async function runSubagentWithProgress(deps, task, reportProgress, signal) {
1607
1696
  const startTime = Date.now();
@@ -1746,12 +1835,19 @@ ${formatSkillsForPrompt(deps.skills ?? [])}`;
1746
1835
  const lastAssistant = [...messages].reverse().find((m) => m?.role === "assistant");
1747
1836
  const terminalStopReason = lastAssistant?.stopReason;
1748
1837
  const terminalError = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
1749
- const collectedResponse = collectLastAssistantText(messages);
1750
- if (terminalStopReason === "error" || terminalStopReason === "aborted" || terminalError || !collectedResponse) {
1838
+ const collectedResponse = collectLastAssistantText(messages) || void 0;
1839
+ const streamInterrupted = terminalStopReason === "error" || terminalStopReason === "aborted" || terminalError || !collectedResponse && terminalStopReason !== "stop" || terminalStopReason === "length" && !messageHasOwnText(lastAssistant);
1840
+ if (streamInterrupted) {
1841
+ if (terminalStopReason === "length") {
1842
+ throw new Error(`Subagent output token limit reached (stopReason=length): the provider cut the final response before the report was produced${collectedResponse ? " (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.`);
1843
+ }
1751
1844
  const detail = terminalError ? `: ${terminalError.slice(0, 200)}` : "";
1752
- throw new Error(`Subagent stream interrupted (SSE stream aborted before the report was produced, stopReason=${terminalStopReason ?? "none"}${collectedResponse ? ", partial transcript discarded" : ", no text response"})${detail}`);
1845
+ throw new Error(`Subagent response failed (provider/transport failure before the report was produced, stopReason=${terminalStopReason ?? "none"}${collectedResponse ? ", partial transcript discarded" : ", no text response"})${detail}`);
1846
+ }
1847
+ let response = collectedResponse ?? "[No textual report: the subagent finished normally (stopReason=stop) but produced no text \u2014 every assistant message was tool calls only. Review the task's effects directly, or rerun it with an explicit instruction to summarize findings.]";
1848
+ if (!collectedResponse) {
1849
+ task.confidenceSignals.push({ type: "no_text_report", message: "subagent finished normally without any textual report" });
1753
1850
  }
1754
- let response = collectedResponse;
1755
1851
  const outputTruncated = terminalStopReason === "length";
1756
1852
  if (outputTruncated) {
1757
1853
  task.confidenceSignals.push({ type: "output_truncated", message: "final response hit the provider's output token limit" });
@@ -1904,25 +2000,6 @@ function normalizeTaskDefs(args) {
1904
2000
  return normalized;
1905
2001
  }
1906
2002
  var DEFAULT_MAX_CONCURRENT_SUBAGENTS = 4;
1907
- var MAX_TRANSIENT_RETRIES = 2;
1908
- var TRANSIENT_RETRY_BACKOFF_MS = 2e3;
1909
- var TRANSIENT_ERROR_PATTERN = /econnreset|econnaborted|etimedout|timeout|enotfound|eai_again|econnrefused|socket hang up|sse stream aborted|fetch failed|status 5\d\d|bad gateway|service unavailable|overloaded/i;
1910
- function isTransientNetworkError(message) {
1911
- return TRANSIENT_ERROR_PATTERN.test(message);
1912
- }
1913
- function sleep(ms, signal) {
1914
- return new Promise((resolve3) => {
1915
- const timer = setTimeout(resolve3, ms);
1916
- signal?.addEventListener(
1917
- "abort",
1918
- () => {
1919
- clearTimeout(timer);
1920
- resolve3();
1921
- },
1922
- { once: true }
1923
- );
1924
- });
1925
- }
1926
2003
  async function runWithConcurrencyLimit(jobs, limit) {
1927
2004
  let next = 0;
1928
2005
  const workers = Array.from({ length: Math.max(1, Math.min(limit, jobs.length)) }, async () => {
@@ -2011,24 +2088,23 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
2011
2088
  toolContext.reportProgress?.({ message: `Launching ${tasks.length} parallel subagents...`, increment: 5 });
2012
2089
  const startTime = Date.now();
2013
2090
  const runTask = async (task) => {
2014
- for (let attempt = 0; ; attempt++) {
2015
- await runSubagentWithProgress(deps, task, toolContext.reportProgress, signal);
2016
- const canRetry = task.mode === "read" && task.status === "failed" && isTransientNetworkError(task.error ?? "") && attempt < MAX_TRANSIENT_RETRIES && !signal?.aborted;
2017
- if (!canRetry) return;
2018
- const failureReason = task.error ?? "unknown";
2019
- task.evidence.length = 0;
2020
- task.runtimeEvidence.length = 0;
2021
- task.reportedEvidence.length = 0;
2022
- task.confidenceSignals.length = 0;
2023
- task.status = "pending";
2024
- task.error = void 0;
2025
- task.completedAt = void 0;
2026
- const backoffMs = (deps.transientRetryBackoffMs ?? TRANSIENT_RETRY_BACKOFF_MS) * (attempt + 1);
2027
- toolContext.reportProgress?.({ message: `[${task.name}] Transient error, retrying in ${Math.round(backoffMs / 1e3)}s (attempt ${attempt + 2}/${MAX_TRANSIENT_RETRIES + 1})...` });
2028
- deps.log(`[subagent] ${task.name} failed with a transient error (${failureReason}); retrying`);
2029
- await sleep(backoffMs, signal);
2030
- if (signal?.aborted) return;
2031
- }
2091
+ await runSubagentWithProgress(deps, task, toolContext.reportProgress, signal);
2092
+ if (task.mode !== "read" || task.status !== "failed" || !task.error) return;
2093
+ if (signal?.aborted) return;
2094
+ if (!isRetryableSubagentFailure(task.error)) return;
2095
+ const settleMs = Math.max(0, (deps.transientRetryBackoffMs ?? 2e3) * 10);
2096
+ toolContext.reportProgress?.({ message: `[${task.name}] Provider/transport failure \u2014 replaying the read task once after ${Math.round(settleMs / 1e3)}s`, increment: 0 });
2097
+ await cancellableBackoff(settleMs, signal);
2098
+ if (signal?.aborted) return;
2099
+ task.evidence = [];
2100
+ task.confidenceSignals = [];
2101
+ task.runtimeEvidence = [];
2102
+ task.reportedEvidence = [];
2103
+ task.error = void 0;
2104
+ task.result = void 0;
2105
+ task.details = void 0;
2106
+ task.parentHandoffRequired = false;
2107
+ await runSubagentWithProgress(deps, task, toolContext.reportProgress, signal);
2032
2108
  };
2033
2109
  await runWithConcurrencyLimit(
2034
2110
  tasks.map((task) => () => runTask(task)),
@@ -3485,6 +3561,7 @@ var FileSessionStore = class {
3485
3561
  }
3486
3562
  delete(sessionId) {
3487
3563
  const paths = this.pathsFor(sessionId);
3564
+ if (sessionDirIsSymlink(paths.dir)) return false;
3488
3565
  if (!this.gateway.exists(paths.dir)) return false;
3489
3566
  this.gateway.deleteDirectory(paths.dir, { recursive: true });
3490
3567
  this.dropCaches();
@@ -4979,6 +5056,163 @@ function createSessionManager(options) {
4979
5056
  return new SessionManager(options);
4980
5057
  }
4981
5058
 
5059
+ // ../../packages/agent-framework/src/observer.ts
5060
+ function notifyObserver(listener, event) {
5061
+ try {
5062
+ void Promise.resolve(listener(event)).catch(() => {
5063
+ });
5064
+ } catch {
5065
+ }
5066
+ }
5067
+
5068
+ // ../../packages/agent-framework/src/session/controller/end-hook.ts
5069
+ async function runEndHook(hook, context, isCurrent, emit) {
5070
+ if (!hook) return;
5071
+ emit({ type: "agent_end_hook_start", hasQueuedMessages: context.hasQueuedMessages });
5072
+ try {
5073
+ await hook(context);
5074
+ if (isCurrent()) emit({ type: "agent_end_hook_end" });
5075
+ } catch (error) {
5076
+ if (isCurrent()) emit({ type: "agent_end_hook_end", errorMessage: error instanceof Error ? error.message : String(error) });
5077
+ }
5078
+ }
5079
+
5080
+ // ../../packages/agent-framework/src/session/controller/convergence.ts
5081
+ var SessionConvergence = class {
5082
+ constructor(options) {
5083
+ this.options = options;
5084
+ }
5085
+ options;
5086
+ generation = 0;
5087
+ active = false;
5088
+ disposed = false;
5089
+ operations = /* @__PURE__ */ new Set();
5090
+ outcome;
5091
+ monitoring = /* @__PURE__ */ new Set();
5092
+ invalidated;
5093
+ invalidation = new Promise((resolve3) => {
5094
+ this.invalidated = resolve3;
5095
+ });
5096
+ get identity() {
5097
+ return { sessionId: this.options.sessionId(), generation: this.generation };
5098
+ }
5099
+ begin() {
5100
+ if (this.disposed || this.active) return;
5101
+ this.active = true;
5102
+ this.outcome = void 0;
5103
+ this.generation++;
5104
+ notifyObserver(this.options.emit, { type: "session_work_started", ...this.identity });
5105
+ }
5106
+ invalidate(reason) {
5107
+ if (this.disposed) return;
5108
+ if (reason === "dispose") this.disposed = true;
5109
+ this.invalidated();
5110
+ this.invalidation = new Promise((resolve3) => {
5111
+ this.invalidated = resolve3;
5112
+ });
5113
+ this.generation++;
5114
+ this.active = false;
5115
+ this.operations.clear();
5116
+ notifyObserver(this.options.emit, { type: "session_invalidated", ...this.identity, reason });
5117
+ }
5118
+ run(action) {
5119
+ if (this.disposed) return Promise.reject(new Error("Controller disposed"));
5120
+ const wasActive = this.active;
5121
+ const preBeginGeneration = this.generation;
5122
+ this.begin();
5123
+ if (this.disposed) return Promise.reject(new Error("Controller disposed"));
5124
+ if (!wasActive && (!this.active || this.generation !== preBeginGeneration + 1)) {
5125
+ return Promise.resolve();
5126
+ }
5127
+ const generation = this.generation;
5128
+ let resolve3;
5129
+ let reject;
5130
+ const operation = new Promise((yes, no) => {
5131
+ resolve3 = yes;
5132
+ reject = no;
5133
+ });
5134
+ this.operations.add(operation);
5135
+ try {
5136
+ resolve3(action());
5137
+ } catch (error) {
5138
+ reject(error);
5139
+ }
5140
+ void operation.catch(() => {
5141
+ if (generation === this.generation) this.outcome = "failed";
5142
+ }).finally(() => {
5143
+ this.operations.delete(operation);
5144
+ this.check();
5145
+ });
5146
+ this.check();
5147
+ return operation;
5148
+ }
5149
+ observe(event) {
5150
+ if (event.type === "agent_event" && event.event.type === "agent_start") this.begin();
5151
+ if (event.type === "turn_failed_terminal" || event.type === "turn_policy_error" || event.type === "agent_end_hook_end" && event.errorMessage) this.outcome = "failed";
5152
+ if (event.type === "turn_cancelled") this.outcome = "cancelled";
5153
+ this.check();
5154
+ }
5155
+ async wait() {
5156
+ const generation = this.generation;
5157
+ const invalidation = this.invalidation;
5158
+ while (generation === this.generation && !this.disposed) {
5159
+ await Promise.race([Promise.allSettled([...this.operations]), invalidation]);
5160
+ if (generation !== this.generation || this.disposed) return;
5161
+ await Promise.race([this.options.wait(), invalidation]);
5162
+ if (generation !== this.generation || this.disposed) return;
5163
+ if (!this.operations.size && !this.options.busy()) return;
5164
+ }
5165
+ }
5166
+ check() {
5167
+ const generation = this.generation;
5168
+ if (!this.active || this.disposed || this.monitoring.has(generation)) return;
5169
+ this.monitoring.add(generation);
5170
+ void this.wait().then(() => {
5171
+ if (generation !== this.generation || !this.active || this.disposed) return;
5172
+ this.active = false;
5173
+ notifyObserver(this.options.emit, {
5174
+ type: "session_quiescent",
5175
+ ...this.identity,
5176
+ outcome: this.outcome ?? (this.options.queued() ? "blocked" : "success")
5177
+ });
5178
+ }).catch(() => {
5179
+ if (generation !== this.generation || !this.active || this.disposed) return;
5180
+ this.active = false;
5181
+ notifyObserver(this.options.emit, { type: "session_quiescent", ...this.identity, outcome: "failed" });
5182
+ }).finally(() => this.monitoring.delete(generation));
5183
+ }
5184
+ };
5185
+
5186
+ // ../../packages/agent-framework/src/session/controller/pending-operations.ts
5187
+ var PendingEndOperations = class {
5188
+ operations = /* @__PURE__ */ new Set();
5189
+ get pending() {
5190
+ return this.operations.size > 0;
5191
+ }
5192
+ clear() {
5193
+ this.operations.clear();
5194
+ }
5195
+ async wait() {
5196
+ await Promise.allSettled([...this.operations]);
5197
+ }
5198
+ track(action) {
5199
+ let resolve3;
5200
+ let reject;
5201
+ const operation = new Promise((yes, no) => {
5202
+ resolve3 = yes;
5203
+ reject = no;
5204
+ });
5205
+ this.operations.add(operation);
5206
+ void operation.finally(() => this.operations.delete(operation)).catch(() => {
5207
+ });
5208
+ try {
5209
+ resolve3(action());
5210
+ } catch (error) {
5211
+ reject(error);
5212
+ }
5213
+ }
5214
+ };
5215
+
4982
5216
  // ../../packages/agent-framework/src/session/controller/auto-compact-policy.ts
4983
5217
  function defaultSetTimeout(callback, delay) {
4984
5218
  return setTimeout(callback, delay);
@@ -5078,6 +5312,8 @@ var AgentSessionAutoContinueController = class {
5078
5312
  }
5079
5313
  options;
5080
5314
  autoContinueTimer;
5315
+ generation = 0;
5316
+ cancelExecutionWait;
5081
5317
  pendingAutoContinue;
5082
5318
  resolvePendingAutoContinue;
5083
5319
  get hasPendingAutoContinue() {
@@ -5093,21 +5329,30 @@ var AgentSessionAutoContinueController = class {
5093
5329
  this.ensurePending();
5094
5330
  const setTimer = this.options.retry?.setTimeout ?? defaultSetTimeout2;
5095
5331
  const snapshot = messages.slice();
5332
+ const generation = this.generation;
5096
5333
  this.autoContinueTimer = setTimer(() => {
5097
5334
  this.autoContinueTimer = void 0;
5098
- void this.maybeScheduleAutoContinue(snapshot).finally(() => this.resolvePending());
5335
+ void this.maybeScheduleAutoContinue(snapshot, generation).catch((error) => {
5336
+ if (generation === this.generation && !this.options.getDisposed()) {
5337
+ this.options.emit({ type: "turn_policy_error", message: error instanceof Error ? error.message : String(error) });
5338
+ }
5339
+ }).finally(() => {
5340
+ if (generation === this.generation && this.autoContinueTimer === void 0) this.resolvePending();
5341
+ });
5099
5342
  }, 0);
5100
5343
  }
5101
5344
  cancel() {
5102
- if (this.autoContinueTimer === void 0) {
5103
- return;
5345
+ this.generation++;
5346
+ if (this.autoContinueTimer !== void 0) {
5347
+ const clearTimer = this.options.retry?.clearTimeout ?? defaultClearTimeout2;
5348
+ clearTimer(this.autoContinueTimer);
5349
+ this.autoContinueTimer = void 0;
5104
5350
  }
5105
- const clearTimer = this.options.retry?.clearTimeout ?? defaultClearTimeout2;
5106
- clearTimer(this.autoContinueTimer);
5107
- this.autoContinueTimer = void 0;
5351
+ this.cancelExecutionWait?.();
5352
+ this.cancelExecutionWait = void 0;
5108
5353
  this.resolvePending();
5109
5354
  }
5110
- async maybeScheduleAutoContinue(messages) {
5355
+ async maybeScheduleAutoContinue(messages, generation) {
5111
5356
  if (!this.options.getAutoContinueMessage || this.options.getDisposed() || this.options.getRuntimeGuardTriggered() || this.options.isBlockedByRuntimePolicy() || this.options.hasQueuedMessages() || this.options.isStreaming()) {
5112
5357
  if (this.options.hasQueuedMessages()) {
5113
5358
  this.options.emit({ type: "auto_continue_skipped", reason: "queued_messages" });
@@ -5126,6 +5371,7 @@ var AgentSessionAutoContinueController = class {
5126
5371
  messages,
5127
5372
  hasQueuedMessages: this.options.hasQueuedMessages()
5128
5373
  });
5374
+ if (generation !== this.generation || this.options.getDisposed() || this.options.getRuntimeGuardTriggered()) return;
5129
5375
  const message = result && "role" in result ? result : result?.message;
5130
5376
  const reason = result && "role" in result ? void 0 : result?.reason;
5131
5377
  if (!message) {
@@ -5140,9 +5386,11 @@ var AgentSessionAutoContinueController = class {
5140
5386
  this.options.agent.followUp(message);
5141
5387
  const setTimer = this.options.retry?.setTimeout ?? defaultSetTimeout2;
5142
5388
  await new Promise((resolve3) => {
5389
+ this.cancelExecutionWait = resolve3;
5143
5390
  this.autoContinueTimer = setTimer(() => {
5144
5391
  this.autoContinueTimer = void 0;
5145
- if (this.options.getDisposed()) {
5392
+ this.cancelExecutionWait = void 0;
5393
+ if (generation !== this.generation || this.options.getDisposed()) {
5146
5394
  resolve3();
5147
5395
  return;
5148
5396
  }
@@ -5195,7 +5443,7 @@ async function runAgentSessionCompaction(params) {
5195
5443
  throw new Error("No compaction handler configured");
5196
5444
  }
5197
5445
  const abortedResult = () => ({ aborted: true });
5198
- const isAborted = () => params.signal?.aborted === true;
5446
+ const isAborted = () => params.signal?.aborted === true || params.isCurrent?.() === false;
5199
5447
  params.emit({ type: "auto_compaction_start", reason: params.reason, willRetry: params.willRetry });
5200
5448
  try {
5201
5449
  if (isAborted()) {
@@ -5336,7 +5584,7 @@ async function runPreRequestAutoCompact(params) {
5336
5584
  try {
5337
5585
  await params.runAutoCompact();
5338
5586
  } catch {
5339
- params.setRecoveryState("none");
5587
+ if (params.isCurrent?.() !== false) params.setRecoveryState("none");
5340
5588
  }
5341
5589
  }
5342
5590
 
@@ -5510,6 +5758,7 @@ var AgentSessionQueueDispatcher = class {
5510
5758
  }
5511
5759
  options;
5512
5760
  dispatchTimer;
5761
+ generation = 0;
5513
5762
  pendingDispatch;
5514
5763
  resolvePendingDispatch;
5515
5764
  get hasPendingDispatch() {
@@ -5524,25 +5773,32 @@ var AgentSessionQueueDispatcher = class {
5524
5773
  }
5525
5774
  this.ensurePending();
5526
5775
  const setTimer = this.options.retry?.setTimeout ?? defaultSetTimeout3;
5776
+ const generation = this.generation;
5527
5777
  this.dispatchTimer = setTimer(() => {
5528
5778
  this.dispatchTimer = void 0;
5529
- void this.dispatchNext().finally(() => this.resolvePending());
5779
+ void this.dispatchNext().catch((error) => {
5780
+ if (generation === this.generation) this.options.emit({ type: "turn_policy_error", message: String(error) });
5781
+ }).finally(() => {
5782
+ if (generation === this.generation) this.resolvePending();
5783
+ });
5530
5784
  }, delayMs);
5531
5785
  }
5532
5786
  cancel() {
5533
- if (this.dispatchTimer === void 0) {
5534
- return;
5787
+ this.generation++;
5788
+ if (this.dispatchTimer !== void 0) {
5789
+ const clearTimer = this.options.retry?.clearTimeout ?? defaultClearTimeout3;
5790
+ clearTimer(this.dispatchTimer);
5791
+ this.dispatchTimer = void 0;
5535
5792
  }
5536
- const clearTimer = this.options.retry?.clearTimeout ?? defaultClearTimeout3;
5537
- clearTimer(this.dispatchTimer);
5538
- this.dispatchTimer = void 0;
5539
5793
  this.resolvePending();
5540
5794
  }
5541
5795
  async dispatchNext() {
5796
+ const generation = this.generation;
5542
5797
  if (this.options.getDisposed() || this.options.isStreaming() || this.options.isBlockedByRuntimePolicy()) {
5543
5798
  return false;
5544
5799
  }
5545
5800
  await this.options.agent.waitForIdle();
5801
+ if (generation !== this.generation) return false;
5546
5802
  if (this.options.getDisposed() || this.options.isStreaming() || this.options.isBlockedByRuntimePolicy()) {
5547
5803
  this.schedule(100);
5548
5804
  return false;
@@ -5557,6 +5813,7 @@ var AgentSessionQueueDispatcher = class {
5557
5813
  cwd: this.options.cwd,
5558
5814
  queuedInput: cloneQueuedInput(queuedInput)
5559
5815
  });
5816
+ if (generation !== this.generation) return false;
5560
5817
  const dispatchDecision = typeof decision === "boolean" ? { allowed: decision } : decision ?? { allowed: true };
5561
5818
  if (!dispatchDecision.allowed) {
5562
5819
  this.options.emit({
@@ -5570,6 +5827,7 @@ var AgentSessionQueueDispatcher = class {
5570
5827
  return false;
5571
5828
  }
5572
5829
  await this.options.runPreRequestAutoCompact();
5830
+ if (generation !== this.generation) return false;
5573
5831
  if (this.options.getDisposed() || this.options.isStreaming() || this.options.isBlockedByRuntimePolicy()) {
5574
5832
  this.schedule(100);
5575
5833
  return false;
@@ -5593,6 +5851,7 @@ var AgentSessionQueueDispatcher = class {
5593
5851
  clearEditor: dispatched.clearEditor,
5594
5852
  visibility: dispatched.visibility
5595
5853
  });
5854
+ if (generation !== this.generation) return false;
5596
5855
  if (!prepared.handled) {
5597
5856
  if (prepared.systemPrompt !== void 0) {
5598
5857
  this.options.agent.setSystemPrompt(prepared.systemPrompt);
@@ -5701,7 +5960,12 @@ var AgentSessionRetryController = class {
5701
5960
  removeTrailingAssistantError;
5702
5961
  continueTurn;
5703
5962
  setRecoveryState;
5704
- retryAttempt = 0;
5963
+ /** Attempts consumed per failure KIND: budgets are granted per kind (a
5964
+ * 10-attempt network budget must not be eaten by interleaved provider
5965
+ * errors, and vice versa), so each kind counts its own attempts. */
5966
+ retryAttempts = /* @__PURE__ */ new Map();
5967
+ /** Total attempts across kinds in this recovery episode, for reporting. */
5968
+ totalRetryAttempts = 0;
5705
5969
  retryTimer;
5706
5970
  pendingRetry;
5707
5971
  resolvePendingRetry;
@@ -5720,36 +5984,41 @@ var AgentSessionRetryController = class {
5720
5984
  return;
5721
5985
  }
5722
5986
  if (!failure) {
5723
- if (this.retryAttempt > 0) {
5724
- this.emit({ type: "retry_succeeded", attempts: this.retryAttempt });
5725
- this.emit({ type: "turn_retry_succeeded", attempts: this.retryAttempt });
5726
- this.retryAttempt = 0;
5987
+ if (this.totalRetryAttempts > 0) {
5988
+ this.emit({ type: "retry_succeeded", attempts: this.totalRetryAttempts });
5989
+ this.emit({ type: "turn_retry_succeeded", attempts: this.totalRetryAttempts });
5990
+ this.resetAttempts();
5727
5991
  this.setRecoveryState("none");
5728
5992
  this.resolvePending();
5729
5993
  }
5730
5994
  return;
5731
5995
  }
5732
5996
  if (!failure.retryable) {
5733
- if (this.retryAttempt > 0) {
5734
- this.retryAttempt = 0;
5997
+ if (this.totalRetryAttempts > 0) {
5998
+ this.resetAttempts();
5735
5999
  this.setRecoveryState("none");
5736
6000
  this.resolvePending();
5737
6001
  }
5738
6002
  return;
5739
6003
  }
5740
6004
  const maxRetries = this.retry.maxRetriesForFailure?.(failure) ?? this.retry.maxRetries ?? 3;
5741
- this.retryAttempt += 1;
5742
- if (this.retryAttempt > maxRetries) {
6005
+ const kindAttempts = (this.retryAttempts.get(failure.kind) ?? 0) + 1;
6006
+ this.retryAttempts.set(failure.kind, kindAttempts);
6007
+ this.totalRetryAttempts += 1;
6008
+ const totalBudget = this.retry.maxTotalRetries;
6009
+ const kindExhausted = kindAttempts > maxRetries;
6010
+ const totalExhausted = totalBudget !== void 0 && this.totalRetryAttempts > totalBudget;
6011
+ if (kindExhausted || totalExhausted) {
5743
6012
  const terminalFailure = asTerminalFailure(failure);
5744
- this.emit({ type: "retry_exhausted", errorMessage: failure.message, maxRetries });
6013
+ this.emit({ type: "retry_exhausted", errorMessage: failure.message, maxRetries: totalExhausted ? totalBudget : maxRetries });
5745
6014
  this.emit({ type: "turn_failed_terminal", failure: terminalFailure, message });
5746
6015
  this.emit({ type: "turn_settled" });
5747
- this.retryAttempt = 0;
6016
+ this.resetAttempts();
5748
6017
  this.setRecoveryState("terminal");
5749
6018
  this.resolvePending();
5750
6019
  return;
5751
6020
  }
5752
- const attempt = this.retryAttempt;
6021
+ const attempt = kindAttempts;
5753
6022
  const rawDelayMs = (this.retry.baseDelayMs ?? 2e3) * 2 ** (attempt - 1);
5754
6023
  const delayMs = this.retry.maxDelayMs !== void 0 ? Math.min(rawDelayMs, this.retry.maxDelayMs) : rawDelayMs;
5755
6024
  this.ensurePending();
@@ -5774,7 +6043,7 @@ var AgentSessionRetryController = class {
5774
6043
  });
5775
6044
  this.emit({ type: "turn_failed_terminal", failure: terminalFailure, message });
5776
6045
  this.emit({ type: "turn_settled" });
5777
- this.retryAttempt = 0;
6046
+ this.resetAttempts();
5778
6047
  this.setRecoveryState("terminal");
5779
6048
  this.resolvePending();
5780
6049
  });
@@ -5803,15 +6072,19 @@ var AgentSessionRetryController = class {
5803
6072
  this.emit({ type: "retry_exhausted", errorMessage: terminalFailure.message, maxRetries });
5804
6073
  this.emit({ type: "turn_failed_terminal", failure: terminalFailure });
5805
6074
  this.emit({ type: "turn_settled" });
5806
- this.retryAttempt = 0;
6075
+ this.resetAttempts();
5807
6076
  this.setRecoveryState("terminal");
5808
6077
  this.resolvePending();
5809
6078
  }
5810
6079
  cancel() {
5811
6080
  this.cancelTimerOnly();
5812
- this.retryAttempt = 0;
6081
+ this.resetAttempts();
5813
6082
  this.resolvePending();
5814
6083
  }
6084
+ resetAttempts() {
6085
+ this.retryAttempts.clear();
6086
+ this.totalRetryAttempts = 0;
6087
+ }
5815
6088
  cancelTimerOnly() {
5816
6089
  if (this.retryTimer === void 0) {
5817
6090
  return;
@@ -5993,9 +6266,14 @@ function classifyControllerRuntimeError(params) {
5993
6266
  return createFailureClassification({ errorMessage, phase: params.phase }, params.retry, params.classifyFailure);
5994
6267
  }
5995
6268
  async function runAgentSessionRuntimePhase(params) {
6269
+ const startedGeneration = params.getCurrentGeneration?.();
6270
+ const isStale = () => params.getCurrentGeneration !== void 0 && startedGeneration !== void 0 && params.getCurrentGeneration() !== startedGeneration;
5996
6271
  try {
5997
6272
  return await params.action();
5998
6273
  } catch (error) {
6274
+ if (isStale()) {
6275
+ throw error;
6276
+ }
5999
6277
  const failure = classifyControllerRuntimeError({
6000
6278
  error,
6001
6279
  phase: params.phase,
@@ -6228,6 +6506,7 @@ async function runAgentSessionControllerAutoCompact(params) {
6228
6506
  reason: params.reason,
6229
6507
  willRetry: params.willRetry,
6230
6508
  signal: params.signal,
6509
+ isCurrent: params.isCurrent,
6231
6510
  compaction: params.compaction,
6232
6511
  agent: params.agent,
6233
6512
  sessionManager: params.sessionManager,
@@ -6238,6 +6517,7 @@ async function runAgentSessionControllerAutoCompact(params) {
6238
6517
  markSyncedToAgentState: params.markSyncedToAgentState,
6239
6518
  continueTurn: params.continueTurn
6240
6519
  });
6520
+ if (params.isCurrent?.() === false) return result;
6241
6521
  if (!params.willRetry || result?.aborted) {
6242
6522
  const alreadyCancelled = params.getRecoveryState?.() === "cancelled" || params.getDisposed?.() === true;
6243
6523
  params.setRecoveryState(result?.aborted && cancelOnAbort ? "cancelled" : "none");
@@ -6251,11 +6531,12 @@ async function runAgentSessionControllerAutoCompact(params) {
6251
6531
  params.emit({ type: "turn_settled" });
6252
6532
  }
6253
6533
  if (!result?.aborted && runAfterAgentEndHook) {
6254
- void params.handleAgentEndHook(params.agent.state.messages.slice());
6534
+ await params.handleAgentEndHook(params.agent.state.messages.slice());
6255
6535
  }
6256
6536
  }
6257
6537
  return result;
6258
6538
  } catch (error) {
6539
+ if (params.isCurrent?.() === false) return { aborted: true };
6259
6540
  const errorMessage = error instanceof Error ? error.message : String(error);
6260
6541
  if (!params.willRetry && params.reason !== "overflow") {
6261
6542
  params.setRecoveryState("none");
@@ -6263,7 +6544,7 @@ async function runAgentSessionControllerAutoCompact(params) {
6263
6544
  params.emit({ type: "turn_settled" });
6264
6545
  }
6265
6546
  if (runAfterAgentEndHook) {
6266
- void params.handleAgentEndHook(params.agent.state.messages.slice());
6547
+ await params.handleAgentEndHook(params.agent.state.messages.slice());
6267
6548
  }
6268
6549
  throw error;
6269
6550
  }
@@ -6281,7 +6562,7 @@ async function runAgentSessionControllerAutoCompact(params) {
6281
6562
  params.emit({ type: "turn_settled" });
6282
6563
  throw error;
6283
6564
  } finally {
6284
- params.clearPendingFailure();
6565
+ if (params.isCurrent?.() !== false) params.clearPendingFailure();
6285
6566
  }
6286
6567
  }
6287
6568
 
@@ -6302,8 +6583,22 @@ var AgentSessionController = class {
6302
6583
  unsubscribeSemantic;
6303
6584
  lastSyncedMessageCount = 0;
6304
6585
  disposed = false;
6586
+ transitioning = false;
6587
+ generation = 0;
6588
+ convergence = new SessionConvergence({
6589
+ sessionId: () => this.sessionManager.getActiveSession()?.id,
6590
+ wait: () => this.waitForRuntimePolicies(),
6591
+ busy: () => this.isStreaming || this.hasPendingRuntimePolicy(),
6592
+ queued: () => this.hasQueuedMessages,
6593
+ emit: (event) => this.emit(event)
6594
+ });
6595
+ get workIdentity() {
6596
+ return this.convergence.identity;
6597
+ }
6598
+ pendingEndOperations = new PendingEndOperations();
6305
6599
  baseSystemPrompt;
6306
6600
  prepareTurn;
6601
+ onExecutionEnd;
6307
6602
  afterAgentEnd;
6308
6603
  getAutoCompactDecision;
6309
6604
  classifyFailure;
@@ -6318,6 +6613,7 @@ var AgentSessionController = class {
6318
6613
  this.baseSystemPrompt = options.baseSystemPrompt ?? this.agent.state.systemPrompt;
6319
6614
  this.prepareTurn = options.prepareTurn;
6320
6615
  this.afterAgentEnd = options.afterAgentEnd;
6616
+ this.onExecutionEnd = options.onExecutionEnd;
6321
6617
  this.getAutoCompactDecision = options.getAutoCompactDecision;
6322
6618
  this.classifyFailure = options.classifyFailure;
6323
6619
  if (options.onEvent) {
@@ -6395,7 +6691,7 @@ var AgentSessionController = class {
6395
6691
  return this.agent.state.isStreaming;
6396
6692
  }
6397
6693
  get hasQueuedMessages() {
6398
- return this.queue.hasQueuedMessages;
6694
+ return this.queue.hasQueuedMessages || this.agent.hasQueuedMessages();
6399
6695
  }
6400
6696
  get pendingMessageCount() {
6401
6697
  return this.queue.length;
@@ -6427,18 +6723,30 @@ var AgentSessionController = class {
6427
6723
  };
6428
6724
  }
6429
6725
  subscribe(listener) {
6726
+ if (this.disposed) return () => {
6727
+ };
6430
6728
  this.listeners.add(listener);
6431
6729
  return () => this.listeners.delete(listener);
6432
6730
  }
6433
6731
  syncFromActiveSession(messages) {
6434
- const activeSession = this.sessionManager.getActiveSession();
6435
- this.agent.sessionId = activeSession?.id;
6436
- const sessionMessages = messages ?? this.sessionManager.getMessages();
6437
- const conversationMessages = getConversationMessages(sessionMessages);
6438
- this.agent.replaceMessages(conversationMessages);
6439
- this.lastSyncedMessageCount = conversationMessages.length;
6440
- this.queue.clear();
6441
- this.recordLifecycle({ type: "session_synced" });
6732
+ if (this.disposed || this.transitioning) return;
6733
+ this.transitioning = true;
6734
+ try {
6735
+ this.generation++;
6736
+ this.cancelScheduledWork();
6737
+ this.pendingEndOperations.clear();
6738
+ const activeSession = this.sessionManager.getActiveSession();
6739
+ this.agent.sessionId = activeSession?.id;
6740
+ const sessionMessages = messages ?? this.sessionManager.getMessages();
6741
+ const conversationMessages = getConversationMessages(sessionMessages);
6742
+ this.agent.replaceMessages(conversationMessages);
6743
+ this.lastSyncedMessageCount = conversationMessages.length;
6744
+ this.queue.clear();
6745
+ this.recordLifecycle({ type: "session_synced" });
6746
+ this.convergence.invalidate("sync");
6747
+ } finally {
6748
+ this.transitioning = false;
6749
+ }
6442
6750
  }
6443
6751
  markSyncedToAgentState() {
6444
6752
  this.lastSyncedMessageCount = this.agent.state.messages.length;
@@ -6456,7 +6764,12 @@ var AgentSessionController = class {
6456
6764
  this.agent.setTools(context.tools);
6457
6765
  }
6458
6766
  }
6459
- async prompt(input, options) {
6767
+ prompt(input, options) {
6768
+ if (this.disposed || this.transitioning) return Promise.reject(new Error("Controller disposed or transitioning"));
6769
+ return this.convergence.run(() => this.promptInternal(input, options));
6770
+ }
6771
+ async promptInternal(input, options) {
6772
+ const generation = this.generation;
6460
6773
  const messages = normalizePromptInput(input);
6461
6774
  if (this.isRecoveryActive()) {
6462
6775
  this.queue.enqueue(messages, options?.streamingBehavior ?? "followUp", {
@@ -6481,8 +6794,9 @@ var AgentSessionController = class {
6481
6794
  throw new Error("Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.");
6482
6795
  }
6483
6796
  await this.runPreRequestAutoCompactIfNeeded();
6797
+ if (generation !== this.generation || this.disposed) return;
6484
6798
  const prepared = await this.runRuntimePhase("prepare", () => this.preparePromptMessages(messages, options));
6485
- if (prepared.handled) {
6799
+ if (generation !== this.generation || this.disposed || prepared.handled) {
6486
6800
  return;
6487
6801
  }
6488
6802
  this.recordLifecycle({ type: "prompt_started" });
@@ -6490,45 +6804,66 @@ var AgentSessionController = class {
6490
6804
  this.agent.setSystemPrompt(prepared.systemPrompt);
6491
6805
  }
6492
6806
  await this.runRuntimePhase("request", () => this.agent.prompt(prepared.messages));
6807
+ if (generation !== this.generation || this.disposed) return;
6493
6808
  await this.runRuntimePhase("post_turn", () => this.syncNewAgentMessages());
6494
6809
  }
6495
- async continue() {
6810
+ continue() {
6811
+ return this.disposed || this.transitioning ? Promise.reject(new Error("Controller disposed or transitioning")) : this.convergence.run(() => this.continueInternal());
6812
+ }
6813
+ async continueInternal() {
6814
+ const generation = this.generation;
6496
6815
  if (this.isStreaming) {
6497
6816
  throw new Error("Agent is already processing. Wait for completion before continuing.");
6498
6817
  }
6499
6818
  await this.runPreRequestAutoCompactIfNeeded();
6819
+ if (generation !== this.generation || this.disposed) return;
6500
6820
  this.recordLifecycle({ type: "prompt_started" });
6501
6821
  await this.runRuntimePhase("request", () => this.agent.continue());
6822
+ if (generation !== this.generation || this.disposed) return;
6502
6823
  await this.runRuntimePhase("post_turn", () => this.syncNewAgentMessages());
6503
6824
  }
6504
6825
  abort() {
6505
- this.recordLifecycle({ type: "abort" });
6506
- const cancelPendingRecovery = this.isRecoveryActive() && !this.isStreaming;
6507
- this.retryController.cancel();
6508
- this.autoCompact.cancel();
6509
- this.queueDispatcher.cancel();
6510
- this.autoContinue.cancel();
6511
- if (cancelPendingRecovery) {
6512
- this.recoveryState = "cancelled";
6513
- this.emit({ type: "turn_cancelled", reason: "user_abort" });
6514
- this.emit({ type: "turn_settled" });
6826
+ if (this.disposed || this.transitioning) return;
6827
+ this.transitioning = true;
6828
+ try {
6829
+ this.generation++;
6830
+ this.pendingEndOperations.clear();
6831
+ this.recordLifecycle({ type: "abort" });
6832
+ const cancelPendingRecovery = this.isRecoveryActive() && !this.isStreaming;
6833
+ this.cancelScheduledWork();
6834
+ if (cancelPendingRecovery) {
6835
+ this.recoveryState = "cancelled";
6836
+ this.emit({ type: "turn_cancelled", reason: "user_abort" });
6837
+ this.emit({ type: "turn_settled" });
6838
+ }
6839
+ this.agent.abort();
6840
+ this.convergence.invalidate("abort");
6841
+ } finally {
6842
+ this.transitioning = false;
6515
6843
  }
6516
- this.agent.abort();
6517
6844
  }
6518
6845
  waitForIdle() {
6519
6846
  return this.agent.waitForIdle();
6520
6847
  }
6521
- async waitForSettled() {
6522
- while (true) {
6848
+ waitForSettled() {
6849
+ return this.convergence.wait();
6850
+ }
6851
+ async waitForRuntimePolicies() {
6852
+ const generation = this.generation;
6853
+ while (!this.disposed && generation === this.generation) {
6523
6854
  await this.agent.waitForIdle();
6524
- if (!this.hasPendingRuntimePolicy()) {
6855
+ if (this.disposed || generation !== this.generation || !this.hasPendingRuntimePolicy()) {
6525
6856
  return;
6526
6857
  }
6527
- await Promise.all([this.retryController.waitForRetry(), this.autoCompact.waitForAutoCompact(), this.autoContinue.waitForAutoContinue(), this.queueDispatcher.waitForDispatch()]);
6858
+ await Promise.all([this.pendingEndOperations.wait(), this.retryController.waitForRetry(), this.autoCompact.waitForAutoCompact(), this.autoContinue.waitForAutoContinue(), this.queueDispatcher.waitForDispatch()]);
6528
6859
  }
6529
6860
  }
6530
6861
  async runAutoCompact(reason, willRetry = false, options) {
6531
- return runAgentSessionControllerAutoCompact({
6862
+ const generation = this.generation;
6863
+ if (this.disposed || this.transitioning) throw new Error("Controller disposed or transitioning");
6864
+ const isCurrent = () => generation === this.generation && !this.disposed;
6865
+ return this.convergence.run(() => runAgentSessionControllerAutoCompact({
6866
+ isCurrent,
6532
6867
  reason,
6533
6868
  willRetry,
6534
6869
  signal: options?.signal,
@@ -6537,7 +6872,9 @@ var AgentSessionController = class {
6537
6872
  sessionManager: this.sessionManager,
6538
6873
  cwd: this.cwd,
6539
6874
  pendingFailure: this.pendingCompactionFailure,
6540
- emit: (event) => this.emit(event),
6875
+ emit: (event) => {
6876
+ if (isCurrent()) this.emit(event);
6877
+ },
6541
6878
  setRecoveryState: (state) => {
6542
6879
  this.recoveryState = state;
6543
6880
  },
@@ -6555,19 +6892,19 @@ var AgentSessionController = class {
6555
6892
  emitSettled: options?.emitSettled,
6556
6893
  runAfterAgentEndHook: options?.runAfterAgentEndHook,
6557
6894
  cancelOnAbort: options?.cancelOnAbort
6558
- });
6895
+ }));
6559
6896
  }
6560
6897
  dispose() {
6561
6898
  if (this.disposed) return;
6562
6899
  this.disposed = true;
6563
- this.retryController.cancel();
6564
- this.autoCompact.cancel();
6565
- this.queueDispatcher.cancel();
6566
- this.autoContinue.cancel();
6900
+ this.generation++;
6901
+ this.pendingEndOperations.clear();
6902
+ this.cancelScheduledWork();
6567
6903
  this.unsubscribeAgent?.();
6568
6904
  this.unsubscribeSemantic?.();
6569
- this.listeners.clear();
6570
6905
  this.queue.clear();
6906
+ this.convergence.invalidate("dispose");
6907
+ this.listeners.clear();
6571
6908
  }
6572
6909
  drainQueuedInputsForEditor() {
6573
6910
  return this.queue.drainForEditor();
@@ -6582,10 +6919,16 @@ var AgentSessionController = class {
6582
6919
  this.queue.replaceText(search, replacement);
6583
6920
  }
6584
6921
  requestQueueDispatch() {
6585
- if (this.queue.length > 0) this.queueDispatcher.schedule();
6922
+ if (!this.disposed && !this.transitioning && this.queue.length > 0) {
6923
+ void this.convergence.run(async () => {
6924
+ this.queueDispatcher.schedule();
6925
+ });
6926
+ }
6586
6927
  }
6587
6928
  async runPreRequestAutoCompactIfNeeded() {
6929
+ const generation = this.generation;
6588
6930
  return runPreRequestAutoCompact({
6931
+ isCurrent: () => generation === this.generation && !this.disposed,
6589
6932
  disposed: this.disposed,
6590
6933
  runtimeGuardTriggered: this.runtimeGuard.triggered,
6591
6934
  isStreaming: this.isStreaming,
@@ -6617,11 +6960,11 @@ var AgentSessionController = class {
6617
6960
  this.queue.markMessageDelivered(event.message);
6618
6961
  }
6619
6962
  if (event.type === "turn_end") {
6620
- void this.syncNewAgentMessages().catch((error) => {
6621
- console.error("[AgentSessionController] Session sync failed after turn:", error);
6622
- });
6963
+ this.pendingEndOperations.track(() => this.syncNewAgentMessages().catch((error) => {
6964
+ this.emit({ type: "turn_policy_error", message: String(error) });
6965
+ }));
6623
6966
  this.recordLifecycle({ type: "turn_end_success" });
6624
- void this.handleTurnEndRuntimePolicies(event.message, event.toolResults).catch((error) => handleTurnEndPolicyThrow((event2) => this.emit(event2), error, {
6967
+ this.pendingEndOperations.track(() => this.handleTurnEndRuntimePolicies(event.message, event.toolResults).catch((error) => handleTurnEndPolicyThrow((event2) => this.emit(event2), error, {
6625
6968
  recoveryWasActive: this.isRecoveryActive(),
6626
6969
  cancelPendingRecovery: () => {
6627
6970
  this.retryController.cancel();
@@ -6630,11 +6973,15 @@ var AgentSessionController = class {
6630
6973
  markRecoveryTerminal: () => {
6631
6974
  this.recoveryState = "terminal";
6632
6975
  }
6633
- }));
6976
+ })));
6634
6977
  }
6635
6978
  if (event.type === "agent_end") {
6636
6979
  this.recordLifecycle({ type: "agent_end" });
6637
- void this.handleAgentEndHook(event.messages);
6980
+ const generation = this.generation;
6981
+ this.pendingEndOperations.track(async () => {
6982
+ await runEndHook(this.onExecutionEnd, { messages: event.messages, agent: this.agent, sessionManager: this.sessionManager, cwd: this.cwd, hasQueuedMessages: this.hasQueuedMessages }, () => generation === this.generation && !this.disposed, (e) => this.emit(e));
6983
+ if (generation === this.generation && !this.disposed) await this.handleAgentEndHook(event.messages);
6984
+ });
6638
6985
  this.emit({ type: "streaming_changed", isStreaming: false });
6639
6986
  }
6640
6987
  }
@@ -6690,27 +7037,12 @@ var AgentSessionController = class {
6690
7037
  });
6691
7038
  }
6692
7039
  async handleAgentEndHook(messages) {
7040
+ const generation = this.generation;
6693
7041
  if (this.disposed || this.runtimeGuard.triggered || this.isBlockedByRuntimePolicy()) {
6694
7042
  return;
6695
7043
  }
6696
- if (this.afterAgentEnd) {
6697
- this.emit({ type: "agent_end_hook_start", hasQueuedMessages: this.hasQueuedMessages });
6698
- try {
6699
- await this.afterAgentEnd({
6700
- messages,
6701
- agent: this.agent,
6702
- sessionManager: this.sessionManager,
6703
- cwd: this.cwd,
6704
- hasQueuedMessages: this.hasQueuedMessages
6705
- });
6706
- this.emit({ type: "agent_end_hook_end" });
6707
- } catch (error) {
6708
- this.emit({
6709
- type: "agent_end_hook_end",
6710
- errorMessage: error instanceof Error ? error.message : String(error)
6711
- });
6712
- }
6713
- }
7044
+ await runEndHook(this.afterAgentEnd, { messages, agent: this.agent, sessionManager: this.sessionManager, cwd: this.cwd, hasQueuedMessages: this.hasQueuedMessages }, () => generation === this.generation && !this.disposed, (event) => this.emit(event));
7045
+ if (generation !== this.generation || this.disposed) return;
6714
7046
  if (this.queue.length > 0) {
6715
7047
  this.queueDispatcher.schedule();
6716
7048
  }
@@ -6731,6 +7063,7 @@ var AgentSessionController = class {
6731
7063
  action,
6732
7064
  retry: this.retryController.policy,
6733
7065
  classifyFailure: this.classifyFailure,
7066
+ getCurrentGeneration: () => this.generation,
6734
7067
  emit: (event) => this.emit(event),
6735
7068
  setRecoveryState: (state) => {
6736
7069
  this.recoveryState = state;
@@ -6743,8 +7076,14 @@ var AgentSessionController = class {
6743
7076
  isBlockedByRuntimePolicy() {
6744
7077
  return this.recoveryState !== "none" || this.retryController.hasPendingRetry || this.autoCompact.hasPendingAutoCompact;
6745
7078
  }
7079
+ cancelScheduledWork() {
7080
+ this.retryController.cancel();
7081
+ this.autoCompact.cancel();
7082
+ this.queueDispatcher.cancel();
7083
+ this.autoContinue.cancel();
7084
+ }
6746
7085
  hasPendingRuntimePolicy() {
6747
- return this.retryController.hasPendingRetry || this.autoCompact.hasPendingAutoCompact || this.autoContinue.hasPendingAutoContinue || this.queueDispatcher.hasPendingDispatch;
7086
+ return this.pendingEndOperations.pending || this.retryController.hasPendingRetry || this.autoCompact.hasPendingAutoCompact || this.autoContinue.hasPendingAutoContinue || this.queueDispatcher.hasPendingDispatch;
6748
7087
  }
6749
7088
  scheduleAutoCompact(decision, failure) {
6750
7089
  this.recordLifecycle({ type: "auto_compact_scheduled" });
@@ -6762,10 +7101,7 @@ var AgentSessionController = class {
6762
7101
  if (this.runtimeGuard.triggered) {
6763
7102
  this.runtimeGuard.markTriggered();
6764
7103
  }
6765
- this.retryController.cancel();
6766
- this.autoCompact.cancel();
6767
- this.queueDispatcher.cancel();
6768
- this.autoContinue.cancel();
7104
+ this.cancelScheduledWork();
6769
7105
  this.recoveryState = "terminal";
6770
7106
  this.emit({ type: "runtime_guard_triggered", toolName, repeatCount, reason });
6771
7107
  this.emit({
@@ -6776,13 +7112,13 @@ var AgentSessionController = class {
6776
7112
  this.agent.abort();
6777
7113
  }
6778
7114
  emit(event) {
7115
+ if (!event.type.startsWith("session_work_") && event.type !== "session_quiescent" && event.type !== "session_invalidated") this.convergence.observe(event);
6779
7116
  for (const listener of this.listeners) {
6780
- listener(event);
7117
+ notifyObserver(listener, event);
6781
7118
  }
6782
7119
  }
6783
7120
  recordLifecycle(input) {
6784
7121
  this.lifecycleState = reduceAgentSessionLifecycle(this.lifecycleState, input);
6785
- void this.lifecycleState.phase;
6786
7122
  }
6787
7123
  };
6788
7124
 
@@ -6965,6 +7301,109 @@ async function requestInteraction(request) {
6965
7301
  return await currentInteractionHandler(request);
6966
7302
  }
6967
7303
 
7304
+ // ../../packages/agent-framework/src/attention.ts
7305
+ function same(a, b) {
7306
+ return a?.sessionId === b.sessionId && a?.generation === b.generation;
7307
+ }
7308
+ var AttentionCoordinator = class {
7309
+ constructor(policy = {}) {
7310
+ this.policy = policy;
7311
+ }
7312
+ policy;
7313
+ current;
7314
+ requests = /* @__PURE__ */ new Map();
7315
+ listeners = /* @__PURE__ */ new Set();
7316
+ sequence = 0;
7317
+ userWork = false;
7318
+ consumed = false;
7319
+ disposed = false;
7320
+ get activeRequests() {
7321
+ return [...this.requests.values()].map((r) => ({ ...r }));
7322
+ }
7323
+ subscribe(listener) {
7324
+ if (this.disposed) return () => {
7325
+ };
7326
+ this.listeners.add(listener);
7327
+ return () => this.listeners.delete(listener);
7328
+ }
7329
+ /** Feed controller work events only. Old turn_settled/skipped events are not outcomes. */
7330
+ handle(event) {
7331
+ if (this.disposed || this.current && event.generation < this.current.generation) return;
7332
+ if (event.type === "session_invalidated") {
7333
+ this.current = { sessionId: event.sessionId, generation: event.generation };
7334
+ this.consumed = true;
7335
+ this.clear();
7336
+ return;
7337
+ }
7338
+ if (event.type === "session_work_started") {
7339
+ if (same(this.current, event)) return;
7340
+ this.current = { sessionId: event.sessionId, generation: event.generation };
7341
+ this.consumed = false;
7342
+ this.clear();
7343
+ return;
7344
+ }
7345
+ if (!same(this.current, event) || this.consumed) return;
7346
+ this.consumed = true;
7347
+ const userWork = this.userWork;
7348
+ this.userWork = false;
7349
+ if (event.outcome === "cancelled") this.clear();
7350
+ else if (event.outcome === "failed" || event.outcome === "blocked") this.request(event, event.outcome);
7351
+ else if (this.policy.completion === "accepted-user-work" && userWork) this.request(event, "completed");
7352
+ }
7353
+ /** Call at actual accepted human delivery, including steer/retry; never infer from message role. */
7354
+ acceptUserWork(identity) {
7355
+ if (this.disposed || !same(this.current, identity) || this.consumed) return false;
7356
+ this.userWork = true;
7357
+ return true;
7358
+ }
7359
+ /** Producers retain the returned ID until answered/cancelled. Duplicate IDs do not broadcast twice. */
7360
+ request(identity, reason, id = `attention-${++this.sequence}`) {
7361
+ if (this.disposed || !same(this.current, identity)) return void 0;
7362
+ const existing = this.requests.get(id);
7363
+ if (existing) return { ...existing };
7364
+ const request = { sessionId: identity.sessionId, generation: identity.generation, id, reason };
7365
+ this.requests.set(id, request);
7366
+ this.emit({ type: "attention_requested", ...request });
7367
+ return { ...request };
7368
+ }
7369
+ /** Only the interaction owner resolves; suppressing/defering host presentation is not resolution. */
7370
+ resolve(id, identity, resolution = "answered") {
7371
+ const request = this.requests.get(id);
7372
+ if (!request || !same(request, identity)) return false;
7373
+ this.requests.delete(id);
7374
+ this.emit({ type: "attention_resolved", ...request, resolution });
7375
+ return true;
7376
+ }
7377
+ dispose() {
7378
+ if (this.disposed) return;
7379
+ this.disposed = true;
7380
+ this.current = void 0;
7381
+ this.consumed = true;
7382
+ this.clear();
7383
+ this.listeners.clear();
7384
+ }
7385
+ clear() {
7386
+ const requests = [...this.requests.values()];
7387
+ this.requests.clear();
7388
+ this.userWork = false;
7389
+ for (const request of requests) this.emit({ type: "attention_resolved", ...request, resolution: "invalidated" });
7390
+ }
7391
+ emit(event) {
7392
+ for (const listener of this.listeners) notifyObserver(listener, event);
7393
+ }
7394
+ };
7395
+
7396
+ // ../../packages/agent-framework/src/interaction-attention.ts
7397
+ function interactionAttentionReason(request) {
7398
+ return request.type === "notify" ? void 0 : "input-required";
7399
+ }
7400
+ function interactionAttentionResolution(response) {
7401
+ if (!response || typeof response !== "object") return "cancelled";
7402
+ const result = response;
7403
+ if (result.timedOut || result.skipped || result.unavailable) return "cancelled";
7404
+ return ["confirmed", "selection", "value", "answers"].some((key) => result[key] !== void 0) ? "answered" : "cancelled";
7405
+ }
7406
+
6968
7407
  // ../../packages/agent-framework/src/context-files.ts
6969
7408
  var AGENTS_CONTEXT_FILE_NAME = "AGENTS.md";
6970
7409
  function normalizeContextPath(path4) {
@@ -8827,14 +9266,23 @@ function createFileObservationState(options = {}) {
8827
9266
  function coverageSpansFile(coveredRanges, totalLines) {
8828
9267
  if (totalLines <= 0) return true;
8829
9268
  const merged = mergeLineRanges(coveredRanges);
8830
- return merged.length > 0 && merged[0].startLine <= 1 && merged[merged.length - 1].endLine >= totalLines;
9269
+ return merged.length === 1 && merged[0].startLine <= 1 && merged[0].endLine >= totalLines;
8831
9270
  }
8832
9271
  function summarize(observed) {
8833
9272
  const merged = mergeLineRanges(observed.coveredRanges);
8834
9273
  const coveredLines = merged.reduce((total, range2) => total + (range2.endLine - range2.startLine + 1), 0);
8835
9274
  let firstUnseenLine;
8836
9275
  if (observed.coveredTotalLines > 0 && coveredLines < observed.coveredTotalLines) {
8837
- firstUnseenLine = merged.length > 0 && merged[0].startLine <= 1 ? merged[merged.length - 1].endLine + 1 : 1;
9276
+ firstUnseenLine = 1;
9277
+ if (merged.length > 0 && merged[0].startLine <= 1) {
9278
+ firstUnseenLine = merged[merged.length - 1].endLine + 1;
9279
+ for (let index = 0; index + 1 < merged.length; index++) {
9280
+ if (merged[index + 1].startLine > merged[index].endLine + 1) {
9281
+ firstUnseenLine = merged[index].endLine + 1;
9282
+ break;
9283
+ }
9284
+ }
9285
+ }
8838
9286
  }
8839
9287
  let lastRange;
8840
9288
  for (const range2 of observed.readRanges.values()) {
@@ -8950,22 +9398,32 @@ function createReadTool(cwd, options) {
8950
9398
  ...rootSchema ? { root: rootSchema } : {},
8951
9399
  offset: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_CROSS_HOST_LINE_NUMBER, description: "Line number to start reading from, 1-indexed. Example: 201." })),
8952
9400
  limit: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_CROSS_HOST_LINE_NUMBER, description: "Maximum number of lines to read. Example: 120." })),
9401
+ tail: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_CROSS_HOST_LINE_NUMBER, description: "Read the last N lines of the file, without needing to know the total line count. Useful for logs and files that grow. Mutually exclusive with offset/limit. Example: tail=50." })),
8953
9402
  question: Type.Optional(Type.String({ description: "Optional focused question for image, video, audio, or document understanding. Use this when asking what a media/document file contains; omit for ordinary text reads." }))
8954
9403
  });
8955
9404
  const pathResolutionDescription = buildPathResolutionDescription(cwd, options);
8956
9405
  return {
8957
9406
  name: "read",
8958
9407
  label: "read",
8959
- description: `Read a file's contents. Use this before editing, and use offset/limit for large files. Images are attached for the current model when it supports vision; otherwise a configured file-understanding service analyzes them. Video, audio, and documents require a configured understanding service. Pass question to specify what to focus on; it does not force a separate model request. Example: read_file path='Scripts/PlayerController.cs' or read_file path='Data/config.json'. ${pathResolutionDescription} For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). When you need the full file, continue with a larger offset until complete.`,
9408
+ description: `Read a file's contents. Use this before editing, and use offset/limit for large files, or tail=N to read the last N lines when the line count is unknown. Images are attached for the current model when it supports vision; otherwise a configured file-understanding service analyzes them. Video, audio, and documents require a configured understanding service. Pass question to specify what to focus on; it does not force a separate model request. Example: read_file path='Scripts/PlayerController.cs' or read_file path='Data/config.json'. ${pathResolutionDescription} For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). When you need the full file, continue with a larger offset until complete.`,
8960
9409
  parameters: readSchema,
8961
9410
  execute: async (_toolCallId, params, signal) => {
8962
- const { path: path4, root, offset, limit, question } = params;
9411
+ const { path: path4, root, question } = params;
9412
+ const tail = params.tail;
9413
+ let offset = params.offset;
9414
+ let limit = params.limit;
8963
9415
  if (offset !== void 0 && (!Number.isSafeInteger(offset) || offset < 1 || offset > MAX_CROSS_HOST_LINE_NUMBER)) {
8964
9416
  return createReadErrorResult(`Invalid arguments: offset must be a positive safe integer no greater than ${MAX_CROSS_HOST_LINE_NUMBER}.`);
8965
9417
  }
8966
9418
  if (limit !== void 0 && (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_CROSS_HOST_LINE_NUMBER)) {
8967
9419
  return createReadErrorResult(`Invalid arguments: limit must be a positive safe integer no greater than ${MAX_CROSS_HOST_LINE_NUMBER}.`);
8968
9420
  }
9421
+ if (tail !== void 0 && (!Number.isSafeInteger(tail) || tail < 1 || tail > MAX_CROSS_HOST_LINE_NUMBER)) {
9422
+ return createReadErrorResult(`Invalid arguments: tail must be a positive safe integer no greater than ${MAX_CROSS_HOST_LINE_NUMBER}.`);
9423
+ }
9424
+ if (tail !== void 0 && (offset !== void 0 || limit !== void 0)) {
9425
+ return createReadErrorResult("Invalid arguments: tail cannot be combined with offset or limit. Use tail=N to read the last N lines, or offset/limit for a window.");
9426
+ }
8969
9427
  if (offset !== void 0 && limit !== void 0 && offset - 1 + limit > MAX_CROSS_HOST_LINE_NUMBER) {
8970
9428
  return createReadErrorResult(`Invalid arguments: offset and limit exceed the ${MAX_CROSS_HOST_LINE_NUMBER}-line cross-host range.`);
8971
9429
  }
@@ -9129,14 +9587,38 @@ Focus: ${question.trim()}` : ""}` },
9129
9587
  });
9130
9588
  return;
9131
9589
  }
9132
- const startLine = offset === void 0 ? 0 : offset - 1;
9590
+ let translatedFromTail = false;
9591
+ let tailWindowIncludesPhantom = false;
9592
+ let tailEffectiveLines;
9593
+ let startLine;
9594
+ let range2;
9595
+ if (tail !== void 0) {
9596
+ const effectiveTail = Math.max(1, Math.min(tail, DEFAULT_MAX_LINES - 1));
9597
+ translatedFromTail = true;
9598
+ tailEffectiveLines = effectiveTail;
9599
+ range2 = await fs2.readTextRange(absolutePath, {
9600
+ tailLines: effectiveTail,
9601
+ maxOutputLines: DEFAULT_MAX_LINES,
9602
+ maxOutputBytes: DEFAULT_MAX_BYTES
9603
+ }, signal);
9604
+ tailWindowIncludesPhantom = range2.endsWithNewline === true;
9605
+ if (range2.selectedLines > 0) {
9606
+ const derivedHead = range2.totalLines - range2.selectedLines + 1;
9607
+ startLine = range2.outputStartLine !== void 0 && !range2.firstLineExceedsLimit ? range2.outputStartLine - 1 : derivedHead - 1;
9608
+ limit = range2.selectedLines;
9609
+ } else {
9610
+ startLine = 0;
9611
+ }
9612
+ } else {
9613
+ startLine = offset === void 0 ? 0 : offset - 1;
9614
+ range2 = await fs2.readTextRange(absolutePath, {
9615
+ offsetLine: startLine,
9616
+ limitLines: limit,
9617
+ maxOutputLines: DEFAULT_MAX_LINES,
9618
+ maxOutputBytes: DEFAULT_MAX_BYTES
9619
+ }, signal);
9620
+ }
9133
9621
  const startLineDisplay = startLine + 1;
9134
- const range2 = await fs2.readTextRange(absolutePath, {
9135
- offsetLine: startLine,
9136
- limitLines: limit,
9137
- maxOutputLines: DEFAULT_MAX_LINES,
9138
- maxOutputBytes: DEFAULT_MAX_BYTES
9139
- }, signal);
9140
9622
  if (options?.allowBinary === false) {
9141
9623
  const selectedLooksBinary = range2.selectedBytesLookBinary ?? (looksLikeBinaryText(range2.content) || range2.content.includes("\uFFFD"));
9142
9624
  const selectedBinary = selectedLooksBinary ? { kind: "document", mimeType: "application/octet-stream" } : void 0;
@@ -9155,7 +9637,7 @@ Focus: ${question.trim()}` : ""}` },
9155
9637
  contentHash: range2.contentHash
9156
9638
  };
9157
9639
  const totalFileLines = range2.totalLines;
9158
- if (startLine >= range2.totalLines) {
9640
+ if (startLine >= range2.totalLines && !translatedFromTail) {
9159
9641
  throw new Error(`Offset ${offset} is beyond end of file (${range2.totalLines} lines total)`);
9160
9642
  }
9161
9643
  let userLimitedLines;
@@ -9176,7 +9658,7 @@ Focus: ${question.trim()}` : ""}` },
9176
9658
  maxLines: DEFAULT_MAX_LINES,
9177
9659
  maxBytes: DEFAULT_MAX_BYTES
9178
9660
  };
9179
- const readStartsAtBeginning = offset === void 0 || offset === 1;
9661
+ const readStartsAtBeginning = startLine === 0;
9180
9662
  const readReachedEndOfFile = startLine + range2.selectedLines >= range2.totalLines;
9181
9663
  const isCompleteTextKnown = readStartsAtBeginning && readReachedEndOfFile && !truncation.truncated && !truncation.firstLineExceedsLimit;
9182
9664
  let callerObservation = fileObservationState;
@@ -9198,13 +9680,14 @@ Focus: ${question.trim()}` : ""}` },
9198
9680
  resolve3({
9199
9681
  content: [{
9200
9682
  type: "text",
9201
- text: `File unchanged since previous read of this range (offset=${startLineDisplay}${limit === void 0 ? ", no limit" : `, limit=${limit}`}). Earlier reads still count toward write_file coverage of this file. Use a different offset/limit if you need another section.`
9683
+ text: translatedFromTail ? `File unchanged since previous tail=${tailEffectiveLines} read (last ${tailWindowIncludesPhantom ? range2.selectedLines - 1 : range2.selectedLines} lines, ${range2.totalLines} lines total). Earlier reads still count toward write_file coverage of this file.` : `File unchanged since previous read of this range (offset=${startLineDisplay}${limit === void 0 ? ", no limit" : `, limit=${limit}`}). Earlier reads still count toward write_file coverage of this file. Use a different offset/limit if you need another section.`
9202
9684
  }],
9203
9685
  details: {
9204
9686
  unchangedSincePreviousRead: true,
9205
9687
  path: absolutePath,
9206
9688
  offset: startLineDisplay,
9207
- limit
9689
+ limit,
9690
+ ...translatedFromTail ? { totalLines: range2.totalLines } : {}
9208
9691
  }
9209
9692
  });
9210
9693
  return;
@@ -9223,7 +9706,12 @@ Base directory: ${nodePath.dirname(absolutePath)}
9223
9706
  const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
9224
9707
  const nextOffset = endLineDisplay + 1;
9225
9708
  outputText = truncation.content;
9226
- if (truncation.truncatedBy === "lines") {
9709
+ if (translatedFromTail) {
9710
+ const endsAtLastLine = truncation.content.length > 0 ? ", and the content above ends at the file's last line" : "";
9711
+ outputText += `
9712
+
9713
+ [Tail window hit the output cap: the earliest lines of the requested tail were dropped${endsAtLastLine}. Read earlier lines with offset, or request a smaller tail.]`;
9714
+ } else if (truncation.truncatedBy === "lines") {
9227
9715
  outputText += `
9228
9716
 
9229
9717
  [Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;
@@ -9249,18 +9737,38 @@ Base directory: ${nodePath.dirname(absolutePath)}
9249
9737
  snapshot,
9250
9738
  range2.selectedContentHash,
9251
9739
  isCompleteTextKnown,
9252
- {
9253
- // firstLineExceedsLimit yields an intentionally empty range
9254
- // (start - 1): zero lines were actually shown, so it must
9255
- // contribute nothing to write coverage.
9256
- startLine: startLineDisplay,
9257
- endLine: startLineDisplay + (truncation.firstLineExceedsLimit ? 0 : truncation.truncated ? truncation.outputLines : range2.selectedLines) - 1,
9258
- totalLines: range2.totalLines
9259
- }
9740
+ (() => {
9741
+ if (translatedFromTail && truncation.truncated && !truncation.firstLineExceedsLimit) {
9742
+ const outputStart = range2.outputStartLine ?? startLineDisplay + Math.max(0, range2.selectedLines - range2.outputLines);
9743
+ const clampedStart = Math.min(outputStart, range2.totalLines + 1);
9744
+ const clampedEnd = Math.min(range2.totalLines, clampedStart + range2.outputLines - 1);
9745
+ return {
9746
+ startLine: clampedStart,
9747
+ endLine: clampedStart > range2.totalLines ? clampedStart - 1 : clampedEnd,
9748
+ totalLines: range2.totalLines
9749
+ };
9750
+ }
9751
+ return {
9752
+ // firstLineExceedsLimit yields an intentionally empty range
9753
+ // (start - 1): zero lines were actually shown, so it must
9754
+ // contribute nothing to write coverage.
9755
+ startLine: startLineDisplay,
9756
+ endLine: startLineDisplay + (truncation.firstLineExceedsLimit ? 0 : truncation.truncated ? truncation.outputLines : range2.selectedLines) - 1,
9757
+ totalLines: range2.totalLines
9758
+ };
9759
+ })()
9260
9760
  );
9261
9761
  if (callerObservation !== fileObservationState) {
9262
9762
  callerObservation.recordTextRead(absolutePath, rangeKey, snapshot, range2.selectedContentHash, isCompleteTextKnown);
9263
9763
  }
9764
+ if (translatedFromTail && tail !== void 0 && tailEffectiveLines !== void 0 && tailEffectiveLines < tail && startLineDisplay > 1) {
9765
+ outputText += `
9766
+
9767
+ [Tail clamped to ${tailEffectiveLines} lines: the per-read line cap is ${DEFAULT_MAX_LINES}, so the ${startLineDisplay - 1} earliest lines of the file are not shown. Use offset reads to walk earlier lines, or a smaller tail.]`;
9768
+ }
9769
+ if (translatedFromTail) {
9770
+ details = { ...details ?? {}, totalLines: range2.totalLines };
9771
+ }
9264
9772
  outputText = sourceHeader + outputText;
9265
9773
  const textContent = [{ type: "text", text: outputText }];
9266
9774
  if (aborted) return;
@@ -9296,10 +9804,28 @@ function isSpecialDevicePath(path4) {
9296
9804
  function detectUnderstandingKind(filePath) {
9297
9805
  const lower = filePath.toLowerCase();
9298
9806
  if (/\.(mp4|mov|m4v|webm|avi|mkv)$/.test(lower)) {
9299
- return { kind: "video", mimeType: lower.endsWith(".mov") ? "video/quicktime" : lower.endsWith(".webm") ? "video/webm" : "video/mp4" };
9807
+ const videoMime = {
9808
+ mp4: "video/mp4",
9809
+ mov: "video/quicktime",
9810
+ m4v: "video/x-m4v",
9811
+ webm: "video/webm",
9812
+ avi: "video/x-msvideo",
9813
+ mkv: "video/x-matroska"
9814
+ };
9815
+ const ext2 = lower.split(".").pop();
9816
+ return { kind: "video", mimeType: videoMime[ext2] ?? "video/mp4" };
9300
9817
  }
9301
9818
  if (/\.(mp3|wav|m4a|aac|flac|ogg)$/.test(lower)) {
9302
- return { kind: "audio", mimeType: lower.endsWith(".wav") ? "audio/wav" : lower.endsWith(".m4a") ? "audio/mp4" : "audio/mpeg" };
9819
+ const audioMime = {
9820
+ mp3: "audio/mpeg",
9821
+ wav: "audio/wav",
9822
+ m4a: "audio/mp4",
9823
+ aac: "audio/aac",
9824
+ flac: "audio/flac",
9825
+ ogg: "audio/ogg"
9826
+ };
9827
+ const ext2 = lower.split(".").pop();
9828
+ return { kind: "audio", mimeType: audioMime[ext2] ?? "audio/mpeg" };
9303
9829
  }
9304
9830
  if (/\.(pdf)$/.test(lower)) {
9305
9831
  return { kind: "document", mimeType: "application/pdf" };
@@ -9621,13 +10147,15 @@ function createWriteTool(cwd, options) {
9621
10147
  const observedSnapshot = fileObservationState.getCompleteTextSnapshot(absolutePath);
9622
10148
  if (!observedSnapshot) {
9623
10149
  const summary = fileObservationState.describeObservation(absolutePath);
9624
- const coverageHint = summary && summary.totalLines > 0 ? ` Previous reads cover ${summary.coveredLines}/${summary.totalLines} lines${summary.lastRange ? ` (last read: lines ${summary.lastRange.startLine}-${summary.lastRange.endLine})` : ""}${summary.firstUnseenLine ? `; read the rest with read_file offset=${summary.firstUnseenLine}` : "; read the full file with read_file (no offset/limit)"}.` : " Read the current full file with read_file (no offset/limit) first.";
10150
+ const coverageHint = summary && summary.totalLines > 0 ? ` Previous reads cover ${summary.coveredLines}/${summary.totalLines} lines${summary.lastRange ? ` (last read: lines ${summary.lastRange.startLine}-${summary.lastRange.endLine})` : ""}${summary.firstUnseenLine ? `; read the rest with read_file offset=${summary.firstUnseenLine}, or a tail read spanning the whole file` : "; read the full file with read_file (no offset/limit, or a tail read spanning the whole file)"}.` : " Read the current full file with read_file (no offset/limit, or a tail read spanning the whole file) first.";
9625
10151
  throw new Error(
9626
10152
  usesExternalFileObservationState ? `write_file blocked: overwriting requires the file to be fully observed by read_file.${coverageHint} Alternatively, use edit_file for targeted changes.` : "Existing-file overwrite requires write_file to share a fileObservationState with read_file; use createSharedFileSystemTools or pass the same fileObservationState to both tools."
9627
10153
  );
9628
10154
  }
9629
10155
  if (!snapshotsHaveSameContent(observedSnapshot, currentSnapshot)) {
9630
- throw new Error("File changed since it was read. Read the latest file contents before overwriting it.");
10156
+ throw new Error(
10157
+ `File changed since it was read (${observedSnapshot.size} \u2192 ${currentSnapshot.size} bytes; out-of-band edits such as bash sed/redirects cause this). Re-read with the read_file tool \u2014 offset/limit/tail reads accumulate coverage; bash head/cat does not count \u2014 or use edit_file for targeted changes.`
10158
+ );
9631
10159
  }
9632
10160
  }
9633
10161
  await fs2.writeFile(absolutePath, content, "utf-8");
@@ -9641,7 +10169,7 @@ function createWriteTool(cwd, options) {
9641
10169
  signal.removeEventListener("abort", onAbort);
9642
10170
  }
9643
10171
  resolve3({
9644
- content: [{ type: "text", text: `Successfully wrote ${content.length} bytes to ${filePath}` }],
10172
+ content: [{ type: "text", text: `Successfully wrote ${writtenStat.size} bytes to ${filePath}` }],
9645
10173
  details: void 0
9646
10174
  });
9647
10175
  } catch (error) {
@@ -9674,6 +10202,8 @@ function restoreLineEndings(text, ending) {
9674
10202
  return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
9675
10203
  }
9676
10204
  var MAX_FUZZY_DIAGNOSTIC_CHARS = 256 * 1024;
10205
+ var MAX_CLOSEST_DIFF_REGION_LINES = 80;
10206
+ var MAX_CLOSEST_DIFF_LINES = 30;
9677
10207
  var RELAXED_MATCH_WORK_BUDGET = 1e6;
9678
10208
  function normalizeForFuzzyMatch(text) {
9679
10209
  return buildFuzzyIndexMap(text).normalized;
@@ -9904,6 +10434,9 @@ function reindentNewText(newText, qLines, fileLines, window) {
9904
10434
  nIdx.push(i);
9905
10435
  }
9906
10436
  if (nIdx.length === 0) return newText;
10437
+ if (qIdx.length === nIdx.length && qIdx.every((q, pos) => qLines[q].trim() === newLines[nIdx[pos]].trim()) && qIdx.some((q, pos) => leadingWhitespace(qLines[q]) !== leadingWhitespace(newLines[nIdx[pos]]))) {
10438
+ return newText;
10439
+ }
9907
10440
  const counterpart = /* @__PURE__ */ new Map();
9908
10441
  let p = 0;
9909
10442
  while (p < qIdx.length && p < nIdx.length && qLines[qIdx[p]].trim() === newLines[nIdx[p]].trim()) {
@@ -10021,7 +10554,8 @@ function describeClosestRegion(content, oldText) {
10021
10554
  let matched = 0;
10022
10555
  let fi = i;
10023
10556
  let firstDiff;
10024
- for (let qi = firstNonBlank; qi < qLines.length; qi++) {
10557
+ let qi = firstNonBlank;
10558
+ for (; qi < qLines.length; qi++) {
10025
10559
  budget -= 1;
10026
10560
  if (budget < 0) {
10027
10561
  return `Closest-region diagnostics were skipped because the file and the quoted block are too repetitive to compare within the relaxed-match budget \u2014 quote a larger region with distinguishing context lines and retry.`;
@@ -10030,24 +10564,102 @@ function describeClosestRegion(content, oldText) {
10030
10564
  if (!q) continue;
10031
10565
  while (fi < fileLines.length && !fileLines[fi].trim()) fi++;
10032
10566
  if (fi >= fileLines.length || fileLines[fi].trim() !== q) {
10033
- firstDiff = { fileNo: fi + 1, fileLine: fileLines[fi] ?? "<end of file>", qLine: qLines[qi] };
10567
+ firstDiff = { fileNo: fi + 1, fileLine: fileLines[fi] ?? "<end of file>", qLine: qLines[qi], qIdx: qi };
10034
10568
  break;
10035
10569
  }
10036
10570
  matched++;
10037
10571
  fi++;
10038
10572
  }
10039
- if (!best || matched > best.matched) best = { start: i, matched, firstDiff };
10573
+ if (!best || matched > best.matched) best = { start: i, walkEnd: fi, matched, firstDiff };
10040
10574
  }
10041
10575
  const total = qLines.filter((line) => line.trim() !== "").length;
10042
10576
  if (!best) {
10043
10577
  return `No line matching ${JSON.stringify(clip(anchor))} exists in this file \u2014 the text may live in a different file, or the file changed since it was read.`;
10044
10578
  }
10045
- const parts = [`Closest match: file lines ${best.start + 1}\u2013${best.start + total} (${best.matched}/${total} lines matched by trimmed content).`];
10579
+ const regionStart = best.start + 1;
10580
+ let regionEnd = Math.max(regionStart, best.walkEnd + (best.firstDiff ? 1 : 0));
10581
+ const parts = [];
10582
+ let alignment = "unknown";
10583
+ let shiftDelta = 0;
10046
10584
  if (best.firstDiff) {
10047
10585
  parts.push(`First difference at line ${best.firstDiff.fileNo}: file has ${JSON.stringify(clip(best.firstDiff.fileLine.trim()))}, edit expected ${JSON.stringify(clip(best.firstDiff.qLine.trim()))}.`);
10586
+ const suffixIdx = [];
10587
+ for (let s = best.firstDiff.qIdx + 1; s < qLines.length && suffixIdx.length < 3; s++) {
10588
+ if (qLines[s].trim()) suffixIdx.push(s);
10589
+ }
10590
+ if (suffixIdx.length === 0) {
10591
+ alignment = "aligned";
10592
+ } else {
10593
+ budget -= 20;
10594
+ const nextNonBlank = (from) => {
10595
+ let i = from;
10596
+ while (i < fileLines.length && !fileLines[i].trim()) i++;
10597
+ return i;
10598
+ };
10599
+ const tryAlign = (fileStart) => {
10600
+ let score = 0;
10601
+ let fi = fileStart;
10602
+ for (const s of suffixIdx) {
10603
+ fi = nextNonBlank(fi);
10604
+ if (fi >= fileLines.length || fileLines[fi].trim() !== qLines[s].trim()) break;
10605
+ score++;
10606
+ fi++;
10607
+ }
10608
+ return { score, endFileIdx: fi };
10609
+ };
10610
+ const atSameLength = tryAlign(best.walkEnd + 1);
10611
+ if (atSameLength.score === suffixIdx.length) {
10612
+ alignment = "aligned";
10613
+ regionEnd = Math.max(regionEnd, Math.min(atSameLength.endFileIdx, best.start + MAX_CLOSEST_DIFF_REGION_LINES));
10614
+ } else {
10615
+ const diffQuoteLine = best.firstDiff.qLine.trim();
10616
+ const nextFile = nextNonBlank(best.walkEnd + 1);
10617
+ const quoteSkipsAFileLine = nextFile < fileLines.length && fileLines[nextFile].trim() === diffQuoteLine;
10618
+ const shiftedCandidates = [best.walkEnd - 1, best.walkEnd, best.walkEnd + 2, best.walkEnd + 3];
10619
+ const shiftedResults = shiftedCandidates.map((candidate) => tryAlign(candidate));
10620
+ let bestShiftedIndex = 0;
10621
+ for (let index = 1; index < shiftedResults.length; index++) {
10622
+ if (shiftedResults[index].score > shiftedResults[bestShiftedIndex].score) bestShiftedIndex = index;
10623
+ }
10624
+ const bestShifted = shiftedResults[bestShiftedIndex];
10625
+ if (quoteSkipsAFileLine) {
10626
+ alignment = "shifted";
10627
+ shiftDelta = nextFile - best.walkEnd;
10628
+ const throughSuffix = tryAlign(nextFile + 1);
10629
+ const end = throughSuffix.score > 0 ? throughSuffix.endFileIdx : nextFile + 1;
10630
+ regionEnd = Math.max(regionEnd, Math.min(end, best.start + MAX_CLOSEST_DIFF_REGION_LINES));
10631
+ } else if (bestShifted.score > atSameLength.score && bestShifted.score === suffixIdx.length) {
10632
+ alignment = "shifted";
10633
+ shiftDelta = shiftedCandidates[bestShiftedIndex] - (best.walkEnd + 1);
10634
+ regionEnd = Math.max(regionEnd, Math.min(bestShifted.endFileIdx, best.start + MAX_CLOSEST_DIFF_REGION_LINES));
10635
+ } else if (atSameLength.score > 0) {
10636
+ alignment = "aligned";
10637
+ regionEnd = Math.max(regionEnd, Math.min(atSameLength.endFileIdx, best.start + MAX_CLOSEST_DIFF_REGION_LINES));
10638
+ }
10639
+ }
10640
+ }
10641
+ if (alignment === "shifted") {
10642
+ parts.push("The remainder of the quote re-aligns only at a SHIFTED position \u2014 an extra, missing, or duplicated line changed the line count; fixing the quoted difference line alone cannot repair the quote.");
10643
+ } else if (alignment === "unknown") {
10644
+ parts.push("The remainder of the quote does not re-align within a few lines of the difference \u2014 the quote is likely stale; re-read the region and re-quote it.");
10645
+ }
10646
+ const quoteSpanLines = qLines.length - firstNonBlank + (alignment === "shifted" ? shiftDelta : 0);
10647
+ regionEnd = Math.max(regionEnd, Math.min(best.start + quoteSpanLines, best.start + MAX_CLOSEST_DIFF_REGION_LINES));
10048
10648
  } else {
10049
10649
  parts.push("All non-empty lines match by trimmed content \u2014 the mismatch is leading whitespace or blank-line placement.");
10050
10650
  }
10651
+ parts.unshift(`Closest match: file lines ${regionStart}\u2013${regionEnd} (${best.matched}/${total} lines matched by trimmed content).`);
10652
+ if (best.firstDiff && alignment !== "unknown" && regionEnd - regionStart + 1 <= MAX_CLOSEST_DIFF_REGION_LINES) {
10653
+ const regionText = fileLines.slice(best.start, Math.min(regionEnd, fileLines.length)).join("\n");
10654
+ const { diff } = generateDiffString(regionText, oldText);
10655
+ const body = diff.split("\n").slice(3);
10656
+ if (body.length > 0) {
10657
+ const shown = body.slice(0, MAX_CLOSEST_DIFF_LINES);
10658
+ const suffix = body.length > shown.length ? ["\u2026"] : [];
10659
+ parts.push(`Diff of the region against the quote (- file, + quote):
10660
+ ${[...shown, ...suffix].join("\n")}`);
10661
+ }
10662
+ }
10051
10663
  return parts.join(" ");
10052
10664
  }
10053
10665
  function generateDiffString(oldContent, newContent, contextLines = 4) {
@@ -13659,13 +14271,14 @@ Focus: ${request.question.trim()}` : ""}` },
13659
14271
  if (request.signal?.aborted) throw new Error("Operation aborted");
13660
14272
  const data = await fs2.readFile(request.absolutePath, "base64");
13661
14273
  if (request.signal?.aborted) throw new Error("Operation aborted");
13662
- if (data) return {
14274
+ const readBytes = Math.floor(data.length * 3 / 4) - (data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0);
14275
+ if (readBytes > 0 && readBytes <= cap && data) return {
13663
14276
  content: [
13664
14277
  { type: "text", text: `Read ${request.kind} file ${request.displayPath} [${request.mimeType}]. Original media attached for the current model.${request.question?.trim() ? `
13665
14278
  Focus: ${request.question.trim()}` : ""}` },
13666
14279
  { type: request.kind, data, mimeType: request.mimeType }
13667
14280
  ],
13668
- details: { understanding: { purpose, kind: request.kind, mimeType: request.mimeType, agentModel: getModelLabel(agentModel), routeSource: "current_model", inputMode: "nativeAttachment", attachmentBytes: size } },
14281
+ details: { understanding: { purpose, kind: request.kind, mimeType: request.mimeType, agentModel: getModelLabel(agentModel), routeSource: "current_model", inputMode: "nativeAttachment", attachmentBytes: readBytes } },
13669
14282
  isError: false
13670
14283
  };
13671
14284
  }
@@ -16702,6 +17315,9 @@ export {
16702
17315
  registerInteractionHandler,
16703
17316
  hasInteractionHandler,
16704
17317
  requestInteraction,
17318
+ AttentionCoordinator,
17319
+ interactionAttentionReason,
17320
+ interactionAttentionResolution,
16705
17321
  AGENTS_CONTEXT_FILE_NAME,
16706
17322
  buildProjectContextSection,
16707
17323
  attachAgentEventsToSessionTrace,