@cydm/pie 2.6.5 → 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-AETQBDEG.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-5OXH6NUH.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) {
@@ -1750,10 +1836,13 @@ ${formatSkillsForPrompt(deps.skills ?? [])}`;
1750
1836
  const terminalStopReason = lastAssistant?.stopReason;
1751
1837
  const terminalError = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
1752
1838
  const collectedResponse = collectLastAssistantText(messages) || void 0;
1753
- const streamInterrupted = terminalStopReason === "error" || terminalStopReason === "aborted" || terminalError || !collectedResponse && terminalStopReason !== "stop";
1839
+ const streamInterrupted = terminalStopReason === "error" || terminalStopReason === "aborted" || terminalError || !collectedResponse && terminalStopReason !== "stop" || terminalStopReason === "length" && !messageHasOwnText(lastAssistant);
1754
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
+ }
1755
1844
  const detail = terminalError ? `: ${terminalError.slice(0, 200)}` : "";
1756
- 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}`);
1757
1846
  }
1758
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.]";
1759
1848
  if (!collectedResponse) {
@@ -1911,25 +2000,6 @@ function normalizeTaskDefs(args) {
1911
2000
  return normalized;
1912
2001
  }
1913
2002
  var DEFAULT_MAX_CONCURRENT_SUBAGENTS = 4;
1914
- var MAX_TRANSIENT_RETRIES = 2;
1915
- var TRANSIENT_RETRY_BACKOFF_MS = 2e3;
1916
- 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;
1917
- function isTransientNetworkError(message) {
1918
- return TRANSIENT_ERROR_PATTERN.test(message);
1919
- }
1920
- function sleep(ms, signal) {
1921
- return new Promise((resolve3) => {
1922
- const timer = setTimeout(resolve3, ms);
1923
- signal?.addEventListener(
1924
- "abort",
1925
- () => {
1926
- clearTimeout(timer);
1927
- resolve3();
1928
- },
1929
- { once: true }
1930
- );
1931
- });
1932
- }
1933
2003
  async function runWithConcurrencyLimit(jobs, limit) {
1934
2004
  let next = 0;
1935
2005
  const workers = Array.from({ length: Math.max(1, Math.min(limit, jobs.length)) }, async () => {
@@ -2018,24 +2088,23 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
2018
2088
  toolContext.reportProgress?.({ message: `Launching ${tasks.length} parallel subagents...`, increment: 5 });
2019
2089
  const startTime = Date.now();
2020
2090
  const runTask = async (task) => {
2021
- for (let attempt = 0; ; attempt++) {
2022
- await runSubagentWithProgress(deps, task, toolContext.reportProgress, signal);
2023
- const canRetry = task.mode === "read" && task.status === "failed" && isTransientNetworkError(task.error ?? "") && attempt < MAX_TRANSIENT_RETRIES && !signal?.aborted;
2024
- if (!canRetry) return;
2025
- const failureReason = task.error ?? "unknown";
2026
- task.evidence.length = 0;
2027
- task.runtimeEvidence.length = 0;
2028
- task.reportedEvidence.length = 0;
2029
- task.confidenceSignals.length = 0;
2030
- task.status = "pending";
2031
- task.error = void 0;
2032
- task.completedAt = void 0;
2033
- const backoffMs = (deps.transientRetryBackoffMs ?? TRANSIENT_RETRY_BACKOFF_MS) * (attempt + 1);
2034
- toolContext.reportProgress?.({ message: `[${task.name}] Transient error, retrying in ${Math.round(backoffMs / 1e3)}s (attempt ${attempt + 2}/${MAX_TRANSIENT_RETRIES + 1})...` });
2035
- deps.log(`[subagent] ${task.name} failed with a transient error (${failureReason}); retrying`);
2036
- await sleep(backoffMs, signal);
2037
- if (signal?.aborted) return;
2038
- }
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);
2039
2108
  };
2040
2109
  await runWithConcurrencyLimit(
2041
2110
  tasks.map((task) => () => runTask(task)),
@@ -4987,6 +5056,163 @@ function createSessionManager(options) {
4987
5056
  return new SessionManager(options);
4988
5057
  }
4989
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
+
4990
5216
  // ../../packages/agent-framework/src/session/controller/auto-compact-policy.ts
4991
5217
  function defaultSetTimeout(callback, delay) {
4992
5218
  return setTimeout(callback, delay);
@@ -5086,6 +5312,8 @@ var AgentSessionAutoContinueController = class {
5086
5312
  }
5087
5313
  options;
5088
5314
  autoContinueTimer;
5315
+ generation = 0;
5316
+ cancelExecutionWait;
5089
5317
  pendingAutoContinue;
5090
5318
  resolvePendingAutoContinue;
5091
5319
  get hasPendingAutoContinue() {
@@ -5101,21 +5329,30 @@ var AgentSessionAutoContinueController = class {
5101
5329
  this.ensurePending();
5102
5330
  const setTimer = this.options.retry?.setTimeout ?? defaultSetTimeout2;
5103
5331
  const snapshot = messages.slice();
5332
+ const generation = this.generation;
5104
5333
  this.autoContinueTimer = setTimer(() => {
5105
5334
  this.autoContinueTimer = void 0;
5106
- 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
+ });
5107
5342
  }, 0);
5108
5343
  }
5109
5344
  cancel() {
5110
- if (this.autoContinueTimer === void 0) {
5111
- 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;
5112
5350
  }
5113
- const clearTimer = this.options.retry?.clearTimeout ?? defaultClearTimeout2;
5114
- clearTimer(this.autoContinueTimer);
5115
- this.autoContinueTimer = void 0;
5351
+ this.cancelExecutionWait?.();
5352
+ this.cancelExecutionWait = void 0;
5116
5353
  this.resolvePending();
5117
5354
  }
5118
- async maybeScheduleAutoContinue(messages) {
5355
+ async maybeScheduleAutoContinue(messages, generation) {
5119
5356
  if (!this.options.getAutoContinueMessage || this.options.getDisposed() || this.options.getRuntimeGuardTriggered() || this.options.isBlockedByRuntimePolicy() || this.options.hasQueuedMessages() || this.options.isStreaming()) {
5120
5357
  if (this.options.hasQueuedMessages()) {
5121
5358
  this.options.emit({ type: "auto_continue_skipped", reason: "queued_messages" });
@@ -5134,6 +5371,7 @@ var AgentSessionAutoContinueController = class {
5134
5371
  messages,
5135
5372
  hasQueuedMessages: this.options.hasQueuedMessages()
5136
5373
  });
5374
+ if (generation !== this.generation || this.options.getDisposed() || this.options.getRuntimeGuardTriggered()) return;
5137
5375
  const message = result && "role" in result ? result : result?.message;
5138
5376
  const reason = result && "role" in result ? void 0 : result?.reason;
5139
5377
  if (!message) {
@@ -5148,9 +5386,11 @@ var AgentSessionAutoContinueController = class {
5148
5386
  this.options.agent.followUp(message);
5149
5387
  const setTimer = this.options.retry?.setTimeout ?? defaultSetTimeout2;
5150
5388
  await new Promise((resolve3) => {
5389
+ this.cancelExecutionWait = resolve3;
5151
5390
  this.autoContinueTimer = setTimer(() => {
5152
5391
  this.autoContinueTimer = void 0;
5153
- if (this.options.getDisposed()) {
5392
+ this.cancelExecutionWait = void 0;
5393
+ if (generation !== this.generation || this.options.getDisposed()) {
5154
5394
  resolve3();
5155
5395
  return;
5156
5396
  }
@@ -5203,7 +5443,7 @@ async function runAgentSessionCompaction(params) {
5203
5443
  throw new Error("No compaction handler configured");
5204
5444
  }
5205
5445
  const abortedResult = () => ({ aborted: true });
5206
- const isAborted = () => params.signal?.aborted === true;
5446
+ const isAborted = () => params.signal?.aborted === true || params.isCurrent?.() === false;
5207
5447
  params.emit({ type: "auto_compaction_start", reason: params.reason, willRetry: params.willRetry });
5208
5448
  try {
5209
5449
  if (isAborted()) {
@@ -5344,7 +5584,7 @@ async function runPreRequestAutoCompact(params) {
5344
5584
  try {
5345
5585
  await params.runAutoCompact();
5346
5586
  } catch {
5347
- params.setRecoveryState("none");
5587
+ if (params.isCurrent?.() !== false) params.setRecoveryState("none");
5348
5588
  }
5349
5589
  }
5350
5590
 
@@ -5518,6 +5758,7 @@ var AgentSessionQueueDispatcher = class {
5518
5758
  }
5519
5759
  options;
5520
5760
  dispatchTimer;
5761
+ generation = 0;
5521
5762
  pendingDispatch;
5522
5763
  resolvePendingDispatch;
5523
5764
  get hasPendingDispatch() {
@@ -5532,25 +5773,32 @@ var AgentSessionQueueDispatcher = class {
5532
5773
  }
5533
5774
  this.ensurePending();
5534
5775
  const setTimer = this.options.retry?.setTimeout ?? defaultSetTimeout3;
5776
+ const generation = this.generation;
5535
5777
  this.dispatchTimer = setTimer(() => {
5536
5778
  this.dispatchTimer = void 0;
5537
- 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
+ });
5538
5784
  }, delayMs);
5539
5785
  }
5540
5786
  cancel() {
5541
- if (this.dispatchTimer === void 0) {
5542
- 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;
5543
5792
  }
5544
- const clearTimer = this.options.retry?.clearTimeout ?? defaultClearTimeout3;
5545
- clearTimer(this.dispatchTimer);
5546
- this.dispatchTimer = void 0;
5547
5793
  this.resolvePending();
5548
5794
  }
5549
5795
  async dispatchNext() {
5796
+ const generation = this.generation;
5550
5797
  if (this.options.getDisposed() || this.options.isStreaming() || this.options.isBlockedByRuntimePolicy()) {
5551
5798
  return false;
5552
5799
  }
5553
5800
  await this.options.agent.waitForIdle();
5801
+ if (generation !== this.generation) return false;
5554
5802
  if (this.options.getDisposed() || this.options.isStreaming() || this.options.isBlockedByRuntimePolicy()) {
5555
5803
  this.schedule(100);
5556
5804
  return false;
@@ -5565,6 +5813,7 @@ var AgentSessionQueueDispatcher = class {
5565
5813
  cwd: this.options.cwd,
5566
5814
  queuedInput: cloneQueuedInput(queuedInput)
5567
5815
  });
5816
+ if (generation !== this.generation) return false;
5568
5817
  const dispatchDecision = typeof decision === "boolean" ? { allowed: decision } : decision ?? { allowed: true };
5569
5818
  if (!dispatchDecision.allowed) {
5570
5819
  this.options.emit({
@@ -5578,6 +5827,7 @@ var AgentSessionQueueDispatcher = class {
5578
5827
  return false;
5579
5828
  }
5580
5829
  await this.options.runPreRequestAutoCompact();
5830
+ if (generation !== this.generation) return false;
5581
5831
  if (this.options.getDisposed() || this.options.isStreaming() || this.options.isBlockedByRuntimePolicy()) {
5582
5832
  this.schedule(100);
5583
5833
  return false;
@@ -5601,6 +5851,7 @@ var AgentSessionQueueDispatcher = class {
5601
5851
  clearEditor: dispatched.clearEditor,
5602
5852
  visibility: dispatched.visibility
5603
5853
  });
5854
+ if (generation !== this.generation) return false;
5604
5855
  if (!prepared.handled) {
5605
5856
  if (prepared.systemPrompt !== void 0) {
5606
5857
  this.options.agent.setSystemPrompt(prepared.systemPrompt);
@@ -6015,9 +6266,14 @@ function classifyControllerRuntimeError(params) {
6015
6266
  return createFailureClassification({ errorMessage, phase: params.phase }, params.retry, params.classifyFailure);
6016
6267
  }
6017
6268
  async function runAgentSessionRuntimePhase(params) {
6269
+ const startedGeneration = params.getCurrentGeneration?.();
6270
+ const isStale = () => params.getCurrentGeneration !== void 0 && startedGeneration !== void 0 && params.getCurrentGeneration() !== startedGeneration;
6018
6271
  try {
6019
6272
  return await params.action();
6020
6273
  } catch (error) {
6274
+ if (isStale()) {
6275
+ throw error;
6276
+ }
6021
6277
  const failure = classifyControllerRuntimeError({
6022
6278
  error,
6023
6279
  phase: params.phase,
@@ -6250,6 +6506,7 @@ async function runAgentSessionControllerAutoCompact(params) {
6250
6506
  reason: params.reason,
6251
6507
  willRetry: params.willRetry,
6252
6508
  signal: params.signal,
6509
+ isCurrent: params.isCurrent,
6253
6510
  compaction: params.compaction,
6254
6511
  agent: params.agent,
6255
6512
  sessionManager: params.sessionManager,
@@ -6260,6 +6517,7 @@ async function runAgentSessionControllerAutoCompact(params) {
6260
6517
  markSyncedToAgentState: params.markSyncedToAgentState,
6261
6518
  continueTurn: params.continueTurn
6262
6519
  });
6520
+ if (params.isCurrent?.() === false) return result;
6263
6521
  if (!params.willRetry || result?.aborted) {
6264
6522
  const alreadyCancelled = params.getRecoveryState?.() === "cancelled" || params.getDisposed?.() === true;
6265
6523
  params.setRecoveryState(result?.aborted && cancelOnAbort ? "cancelled" : "none");
@@ -6273,11 +6531,12 @@ async function runAgentSessionControllerAutoCompact(params) {
6273
6531
  params.emit({ type: "turn_settled" });
6274
6532
  }
6275
6533
  if (!result?.aborted && runAfterAgentEndHook) {
6276
- void params.handleAgentEndHook(params.agent.state.messages.slice());
6534
+ await params.handleAgentEndHook(params.agent.state.messages.slice());
6277
6535
  }
6278
6536
  }
6279
6537
  return result;
6280
6538
  } catch (error) {
6539
+ if (params.isCurrent?.() === false) return { aborted: true };
6281
6540
  const errorMessage = error instanceof Error ? error.message : String(error);
6282
6541
  if (!params.willRetry && params.reason !== "overflow") {
6283
6542
  params.setRecoveryState("none");
@@ -6285,7 +6544,7 @@ async function runAgentSessionControllerAutoCompact(params) {
6285
6544
  params.emit({ type: "turn_settled" });
6286
6545
  }
6287
6546
  if (runAfterAgentEndHook) {
6288
- void params.handleAgentEndHook(params.agent.state.messages.slice());
6547
+ await params.handleAgentEndHook(params.agent.state.messages.slice());
6289
6548
  }
6290
6549
  throw error;
6291
6550
  }
@@ -6303,7 +6562,7 @@ async function runAgentSessionControllerAutoCompact(params) {
6303
6562
  params.emit({ type: "turn_settled" });
6304
6563
  throw error;
6305
6564
  } finally {
6306
- params.clearPendingFailure();
6565
+ if (params.isCurrent?.() !== false) params.clearPendingFailure();
6307
6566
  }
6308
6567
  }
6309
6568
 
@@ -6324,8 +6583,22 @@ var AgentSessionController = class {
6324
6583
  unsubscribeSemantic;
6325
6584
  lastSyncedMessageCount = 0;
6326
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();
6327
6599
  baseSystemPrompt;
6328
6600
  prepareTurn;
6601
+ onExecutionEnd;
6329
6602
  afterAgentEnd;
6330
6603
  getAutoCompactDecision;
6331
6604
  classifyFailure;
@@ -6340,6 +6613,7 @@ var AgentSessionController = class {
6340
6613
  this.baseSystemPrompt = options.baseSystemPrompt ?? this.agent.state.systemPrompt;
6341
6614
  this.prepareTurn = options.prepareTurn;
6342
6615
  this.afterAgentEnd = options.afterAgentEnd;
6616
+ this.onExecutionEnd = options.onExecutionEnd;
6343
6617
  this.getAutoCompactDecision = options.getAutoCompactDecision;
6344
6618
  this.classifyFailure = options.classifyFailure;
6345
6619
  if (options.onEvent) {
@@ -6417,7 +6691,7 @@ var AgentSessionController = class {
6417
6691
  return this.agent.state.isStreaming;
6418
6692
  }
6419
6693
  get hasQueuedMessages() {
6420
- return this.queue.hasQueuedMessages;
6694
+ return this.queue.hasQueuedMessages || this.agent.hasQueuedMessages();
6421
6695
  }
6422
6696
  get pendingMessageCount() {
6423
6697
  return this.queue.length;
@@ -6449,18 +6723,30 @@ var AgentSessionController = class {
6449
6723
  };
6450
6724
  }
6451
6725
  subscribe(listener) {
6726
+ if (this.disposed) return () => {
6727
+ };
6452
6728
  this.listeners.add(listener);
6453
6729
  return () => this.listeners.delete(listener);
6454
6730
  }
6455
6731
  syncFromActiveSession(messages) {
6456
- const activeSession = this.sessionManager.getActiveSession();
6457
- this.agent.sessionId = activeSession?.id;
6458
- const sessionMessages = messages ?? this.sessionManager.getMessages();
6459
- const conversationMessages = getConversationMessages(sessionMessages);
6460
- this.agent.replaceMessages(conversationMessages);
6461
- this.lastSyncedMessageCount = conversationMessages.length;
6462
- this.queue.clear();
6463
- 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
+ }
6464
6750
  }
6465
6751
  markSyncedToAgentState() {
6466
6752
  this.lastSyncedMessageCount = this.agent.state.messages.length;
@@ -6478,7 +6764,12 @@ var AgentSessionController = class {
6478
6764
  this.agent.setTools(context.tools);
6479
6765
  }
6480
6766
  }
6481
- 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;
6482
6773
  const messages = normalizePromptInput(input);
6483
6774
  if (this.isRecoveryActive()) {
6484
6775
  this.queue.enqueue(messages, options?.streamingBehavior ?? "followUp", {
@@ -6503,8 +6794,9 @@ var AgentSessionController = class {
6503
6794
  throw new Error("Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.");
6504
6795
  }
6505
6796
  await this.runPreRequestAutoCompactIfNeeded();
6797
+ if (generation !== this.generation || this.disposed) return;
6506
6798
  const prepared = await this.runRuntimePhase("prepare", () => this.preparePromptMessages(messages, options));
6507
- if (prepared.handled) {
6799
+ if (generation !== this.generation || this.disposed || prepared.handled) {
6508
6800
  return;
6509
6801
  }
6510
6802
  this.recordLifecycle({ type: "prompt_started" });
@@ -6512,45 +6804,66 @@ var AgentSessionController = class {
6512
6804
  this.agent.setSystemPrompt(prepared.systemPrompt);
6513
6805
  }
6514
6806
  await this.runRuntimePhase("request", () => this.agent.prompt(prepared.messages));
6807
+ if (generation !== this.generation || this.disposed) return;
6515
6808
  await this.runRuntimePhase("post_turn", () => this.syncNewAgentMessages());
6516
6809
  }
6517
- 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;
6518
6815
  if (this.isStreaming) {
6519
6816
  throw new Error("Agent is already processing. Wait for completion before continuing.");
6520
6817
  }
6521
6818
  await this.runPreRequestAutoCompactIfNeeded();
6819
+ if (generation !== this.generation || this.disposed) return;
6522
6820
  this.recordLifecycle({ type: "prompt_started" });
6523
6821
  await this.runRuntimePhase("request", () => this.agent.continue());
6822
+ if (generation !== this.generation || this.disposed) return;
6524
6823
  await this.runRuntimePhase("post_turn", () => this.syncNewAgentMessages());
6525
6824
  }
6526
6825
  abort() {
6527
- this.recordLifecycle({ type: "abort" });
6528
- const cancelPendingRecovery = this.isRecoveryActive() && !this.isStreaming;
6529
- this.retryController.cancel();
6530
- this.autoCompact.cancel();
6531
- this.queueDispatcher.cancel();
6532
- this.autoContinue.cancel();
6533
- if (cancelPendingRecovery) {
6534
- this.recoveryState = "cancelled";
6535
- this.emit({ type: "turn_cancelled", reason: "user_abort" });
6536
- 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;
6537
6843
  }
6538
- this.agent.abort();
6539
6844
  }
6540
6845
  waitForIdle() {
6541
6846
  return this.agent.waitForIdle();
6542
6847
  }
6543
- async waitForSettled() {
6544
- 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) {
6545
6854
  await this.agent.waitForIdle();
6546
- if (!this.hasPendingRuntimePolicy()) {
6855
+ if (this.disposed || generation !== this.generation || !this.hasPendingRuntimePolicy()) {
6547
6856
  return;
6548
6857
  }
6549
- 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()]);
6550
6859
  }
6551
6860
  }
6552
6861
  async runAutoCompact(reason, willRetry = false, options) {
6553
- 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,
6554
6867
  reason,
6555
6868
  willRetry,
6556
6869
  signal: options?.signal,
@@ -6559,7 +6872,9 @@ var AgentSessionController = class {
6559
6872
  sessionManager: this.sessionManager,
6560
6873
  cwd: this.cwd,
6561
6874
  pendingFailure: this.pendingCompactionFailure,
6562
- emit: (event) => this.emit(event),
6875
+ emit: (event) => {
6876
+ if (isCurrent()) this.emit(event);
6877
+ },
6563
6878
  setRecoveryState: (state) => {
6564
6879
  this.recoveryState = state;
6565
6880
  },
@@ -6577,19 +6892,19 @@ var AgentSessionController = class {
6577
6892
  emitSettled: options?.emitSettled,
6578
6893
  runAfterAgentEndHook: options?.runAfterAgentEndHook,
6579
6894
  cancelOnAbort: options?.cancelOnAbort
6580
- });
6895
+ }));
6581
6896
  }
6582
6897
  dispose() {
6583
6898
  if (this.disposed) return;
6584
6899
  this.disposed = true;
6585
- this.retryController.cancel();
6586
- this.autoCompact.cancel();
6587
- this.queueDispatcher.cancel();
6588
- this.autoContinue.cancel();
6900
+ this.generation++;
6901
+ this.pendingEndOperations.clear();
6902
+ this.cancelScheduledWork();
6589
6903
  this.unsubscribeAgent?.();
6590
6904
  this.unsubscribeSemantic?.();
6591
- this.listeners.clear();
6592
6905
  this.queue.clear();
6906
+ this.convergence.invalidate("dispose");
6907
+ this.listeners.clear();
6593
6908
  }
6594
6909
  drainQueuedInputsForEditor() {
6595
6910
  return this.queue.drainForEditor();
@@ -6604,10 +6919,16 @@ var AgentSessionController = class {
6604
6919
  this.queue.replaceText(search, replacement);
6605
6920
  }
6606
6921
  requestQueueDispatch() {
6607
- 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
+ }
6608
6927
  }
6609
6928
  async runPreRequestAutoCompactIfNeeded() {
6929
+ const generation = this.generation;
6610
6930
  return runPreRequestAutoCompact({
6931
+ isCurrent: () => generation === this.generation && !this.disposed,
6611
6932
  disposed: this.disposed,
6612
6933
  runtimeGuardTriggered: this.runtimeGuard.triggered,
6613
6934
  isStreaming: this.isStreaming,
@@ -6639,11 +6960,11 @@ var AgentSessionController = class {
6639
6960
  this.queue.markMessageDelivered(event.message);
6640
6961
  }
6641
6962
  if (event.type === "turn_end") {
6642
- void this.syncNewAgentMessages().catch((error) => {
6643
- console.error("[AgentSessionController] Session sync failed after turn:", error);
6644
- });
6963
+ this.pendingEndOperations.track(() => this.syncNewAgentMessages().catch((error) => {
6964
+ this.emit({ type: "turn_policy_error", message: String(error) });
6965
+ }));
6645
6966
  this.recordLifecycle({ type: "turn_end_success" });
6646
- 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, {
6647
6968
  recoveryWasActive: this.isRecoveryActive(),
6648
6969
  cancelPendingRecovery: () => {
6649
6970
  this.retryController.cancel();
@@ -6652,11 +6973,15 @@ var AgentSessionController = class {
6652
6973
  markRecoveryTerminal: () => {
6653
6974
  this.recoveryState = "terminal";
6654
6975
  }
6655
- }));
6976
+ })));
6656
6977
  }
6657
6978
  if (event.type === "agent_end") {
6658
6979
  this.recordLifecycle({ type: "agent_end" });
6659
- 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
+ });
6660
6985
  this.emit({ type: "streaming_changed", isStreaming: false });
6661
6986
  }
6662
6987
  }
@@ -6712,27 +7037,12 @@ var AgentSessionController = class {
6712
7037
  });
6713
7038
  }
6714
7039
  async handleAgentEndHook(messages) {
7040
+ const generation = this.generation;
6715
7041
  if (this.disposed || this.runtimeGuard.triggered || this.isBlockedByRuntimePolicy()) {
6716
7042
  return;
6717
7043
  }
6718
- if (this.afterAgentEnd) {
6719
- this.emit({ type: "agent_end_hook_start", hasQueuedMessages: this.hasQueuedMessages });
6720
- try {
6721
- await this.afterAgentEnd({
6722
- messages,
6723
- agent: this.agent,
6724
- sessionManager: this.sessionManager,
6725
- cwd: this.cwd,
6726
- hasQueuedMessages: this.hasQueuedMessages
6727
- });
6728
- this.emit({ type: "agent_end_hook_end" });
6729
- } catch (error) {
6730
- this.emit({
6731
- type: "agent_end_hook_end",
6732
- errorMessage: error instanceof Error ? error.message : String(error)
6733
- });
6734
- }
6735
- }
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;
6736
7046
  if (this.queue.length > 0) {
6737
7047
  this.queueDispatcher.schedule();
6738
7048
  }
@@ -6753,6 +7063,7 @@ var AgentSessionController = class {
6753
7063
  action,
6754
7064
  retry: this.retryController.policy,
6755
7065
  classifyFailure: this.classifyFailure,
7066
+ getCurrentGeneration: () => this.generation,
6756
7067
  emit: (event) => this.emit(event),
6757
7068
  setRecoveryState: (state) => {
6758
7069
  this.recoveryState = state;
@@ -6765,8 +7076,14 @@ var AgentSessionController = class {
6765
7076
  isBlockedByRuntimePolicy() {
6766
7077
  return this.recoveryState !== "none" || this.retryController.hasPendingRetry || this.autoCompact.hasPendingAutoCompact;
6767
7078
  }
7079
+ cancelScheduledWork() {
7080
+ this.retryController.cancel();
7081
+ this.autoCompact.cancel();
7082
+ this.queueDispatcher.cancel();
7083
+ this.autoContinue.cancel();
7084
+ }
6768
7085
  hasPendingRuntimePolicy() {
6769
- 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;
6770
7087
  }
6771
7088
  scheduleAutoCompact(decision, failure) {
6772
7089
  this.recordLifecycle({ type: "auto_compact_scheduled" });
@@ -6784,10 +7101,7 @@ var AgentSessionController = class {
6784
7101
  if (this.runtimeGuard.triggered) {
6785
7102
  this.runtimeGuard.markTriggered();
6786
7103
  }
6787
- this.retryController.cancel();
6788
- this.autoCompact.cancel();
6789
- this.queueDispatcher.cancel();
6790
- this.autoContinue.cancel();
7104
+ this.cancelScheduledWork();
6791
7105
  this.recoveryState = "terminal";
6792
7106
  this.emit({ type: "runtime_guard_triggered", toolName, repeatCount, reason });
6793
7107
  this.emit({
@@ -6798,13 +7112,13 @@ var AgentSessionController = class {
6798
7112
  this.agent.abort();
6799
7113
  }
6800
7114
  emit(event) {
7115
+ if (!event.type.startsWith("session_work_") && event.type !== "session_quiescent" && event.type !== "session_invalidated") this.convergence.observe(event);
6801
7116
  for (const listener of this.listeners) {
6802
- listener(event);
7117
+ notifyObserver(listener, event);
6803
7118
  }
6804
7119
  }
6805
7120
  recordLifecycle(input) {
6806
7121
  this.lifecycleState = reduceAgentSessionLifecycle(this.lifecycleState, input);
6807
- void this.lifecycleState.phase;
6808
7122
  }
6809
7123
  };
6810
7124
 
@@ -6987,6 +7301,109 @@ async function requestInteraction(request) {
6987
7301
  return await currentInteractionHandler(request);
6988
7302
  }
6989
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
+
6990
7407
  // ../../packages/agent-framework/src/context-files.ts
6991
7408
  var AGENTS_CONTEXT_FILE_NAME = "AGENTS.md";
6992
7409
  function normalizeContextPath(path4) {
@@ -8849,14 +9266,23 @@ function createFileObservationState(options = {}) {
8849
9266
  function coverageSpansFile(coveredRanges, totalLines) {
8850
9267
  if (totalLines <= 0) return true;
8851
9268
  const merged = mergeLineRanges(coveredRanges);
8852
- 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;
8853
9270
  }
8854
9271
  function summarize(observed) {
8855
9272
  const merged = mergeLineRanges(observed.coveredRanges);
8856
9273
  const coveredLines = merged.reduce((total, range2) => total + (range2.endLine - range2.startLine + 1), 0);
8857
9274
  let firstUnseenLine;
8858
9275
  if (observed.coveredTotalLines > 0 && coveredLines < observed.coveredTotalLines) {
8859
- 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
+ }
8860
9286
  }
8861
9287
  let lastRange;
8862
9288
  for (const range2 of observed.readRanges.values()) {
@@ -8972,22 +9398,32 @@ function createReadTool(cwd, options) {
8972
9398
  ...rootSchema ? { root: rootSchema } : {},
8973
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." })),
8974
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." })),
8975
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." }))
8976
9403
  });
8977
9404
  const pathResolutionDescription = buildPathResolutionDescription(cwd, options);
8978
9405
  return {
8979
9406
  name: "read",
8980
9407
  label: "read",
8981
- 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.`,
8982
9409
  parameters: readSchema,
8983
9410
  execute: async (_toolCallId, params, signal) => {
8984
- 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;
8985
9415
  if (offset !== void 0 && (!Number.isSafeInteger(offset) || offset < 1 || offset > MAX_CROSS_HOST_LINE_NUMBER)) {
8986
9416
  return createReadErrorResult(`Invalid arguments: offset must be a positive safe integer no greater than ${MAX_CROSS_HOST_LINE_NUMBER}.`);
8987
9417
  }
8988
9418
  if (limit !== void 0 && (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_CROSS_HOST_LINE_NUMBER)) {
8989
9419
  return createReadErrorResult(`Invalid arguments: limit must be a positive safe integer no greater than ${MAX_CROSS_HOST_LINE_NUMBER}.`);
8990
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
+ }
8991
9427
  if (offset !== void 0 && limit !== void 0 && offset - 1 + limit > MAX_CROSS_HOST_LINE_NUMBER) {
8992
9428
  return createReadErrorResult(`Invalid arguments: offset and limit exceed the ${MAX_CROSS_HOST_LINE_NUMBER}-line cross-host range.`);
8993
9429
  }
@@ -9151,14 +9587,38 @@ Focus: ${question.trim()}` : ""}` },
9151
9587
  });
9152
9588
  return;
9153
9589
  }
9154
- 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
+ }
9155
9621
  const startLineDisplay = startLine + 1;
9156
- const range2 = await fs2.readTextRange(absolutePath, {
9157
- offsetLine: startLine,
9158
- limitLines: limit,
9159
- maxOutputLines: DEFAULT_MAX_LINES,
9160
- maxOutputBytes: DEFAULT_MAX_BYTES
9161
- }, signal);
9162
9622
  if (options?.allowBinary === false) {
9163
9623
  const selectedLooksBinary = range2.selectedBytesLookBinary ?? (looksLikeBinaryText(range2.content) || range2.content.includes("\uFFFD"));
9164
9624
  const selectedBinary = selectedLooksBinary ? { kind: "document", mimeType: "application/octet-stream" } : void 0;
@@ -9177,7 +9637,7 @@ Focus: ${question.trim()}` : ""}` },
9177
9637
  contentHash: range2.contentHash
9178
9638
  };
9179
9639
  const totalFileLines = range2.totalLines;
9180
- if (startLine >= range2.totalLines) {
9640
+ if (startLine >= range2.totalLines && !translatedFromTail) {
9181
9641
  throw new Error(`Offset ${offset} is beyond end of file (${range2.totalLines} lines total)`);
9182
9642
  }
9183
9643
  let userLimitedLines;
@@ -9198,7 +9658,7 @@ Focus: ${question.trim()}` : ""}` },
9198
9658
  maxLines: DEFAULT_MAX_LINES,
9199
9659
  maxBytes: DEFAULT_MAX_BYTES
9200
9660
  };
9201
- const readStartsAtBeginning = offset === void 0 || offset === 1;
9661
+ const readStartsAtBeginning = startLine === 0;
9202
9662
  const readReachedEndOfFile = startLine + range2.selectedLines >= range2.totalLines;
9203
9663
  const isCompleteTextKnown = readStartsAtBeginning && readReachedEndOfFile && !truncation.truncated && !truncation.firstLineExceedsLimit;
9204
9664
  let callerObservation = fileObservationState;
@@ -9220,13 +9680,14 @@ Focus: ${question.trim()}` : ""}` },
9220
9680
  resolve3({
9221
9681
  content: [{
9222
9682
  type: "text",
9223
- 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.`
9224
9684
  }],
9225
9685
  details: {
9226
9686
  unchangedSincePreviousRead: true,
9227
9687
  path: absolutePath,
9228
9688
  offset: startLineDisplay,
9229
- limit
9689
+ limit,
9690
+ ...translatedFromTail ? { totalLines: range2.totalLines } : {}
9230
9691
  }
9231
9692
  });
9232
9693
  return;
@@ -9245,7 +9706,12 @@ Base directory: ${nodePath.dirname(absolutePath)}
9245
9706
  const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
9246
9707
  const nextOffset = endLineDisplay + 1;
9247
9708
  outputText = truncation.content;
9248
- 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") {
9249
9715
  outputText += `
9250
9716
 
9251
9717
  [Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;
@@ -9271,18 +9737,38 @@ Base directory: ${nodePath.dirname(absolutePath)}
9271
9737
  snapshot,
9272
9738
  range2.selectedContentHash,
9273
9739
  isCompleteTextKnown,
9274
- {
9275
- // firstLineExceedsLimit yields an intentionally empty range
9276
- // (start - 1): zero lines were actually shown, so it must
9277
- // contribute nothing to write coverage.
9278
- startLine: startLineDisplay,
9279
- endLine: startLineDisplay + (truncation.firstLineExceedsLimit ? 0 : truncation.truncated ? truncation.outputLines : range2.selectedLines) - 1,
9280
- totalLines: range2.totalLines
9281
- }
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
+ })()
9282
9760
  );
9283
9761
  if (callerObservation !== fileObservationState) {
9284
9762
  callerObservation.recordTextRead(absolutePath, rangeKey, snapshot, range2.selectedContentHash, isCompleteTextKnown);
9285
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
+ }
9286
9772
  outputText = sourceHeader + outputText;
9287
9773
  const textContent = [{ type: "text", text: outputText }];
9288
9774
  if (aborted) return;
@@ -9661,13 +10147,15 @@ function createWriteTool(cwd, options) {
9661
10147
  const observedSnapshot = fileObservationState.getCompleteTextSnapshot(absolutePath);
9662
10148
  if (!observedSnapshot) {
9663
10149
  const summary = fileObservationState.describeObservation(absolutePath);
9664
- 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.";
9665
10151
  throw new Error(
9666
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."
9667
10153
  );
9668
10154
  }
9669
10155
  if (!snapshotsHaveSameContent(observedSnapshot, currentSnapshot)) {
9670
- 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
+ );
9671
10159
  }
9672
10160
  }
9673
10161
  await fs2.writeFile(absolutePath, content, "utf-8");
@@ -9681,7 +10169,7 @@ function createWriteTool(cwd, options) {
9681
10169
  signal.removeEventListener("abort", onAbort);
9682
10170
  }
9683
10171
  resolve3({
9684
- content: [{ type: "text", text: `Successfully wrote ${content.length} bytes to ${filePath}` }],
10172
+ content: [{ type: "text", text: `Successfully wrote ${writtenStat.size} bytes to ${filePath}` }],
9685
10173
  details: void 0
9686
10174
  });
9687
10175
  } catch (error) {
@@ -9714,6 +10202,8 @@ function restoreLineEndings(text, ending) {
9714
10202
  return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
9715
10203
  }
9716
10204
  var MAX_FUZZY_DIAGNOSTIC_CHARS = 256 * 1024;
10205
+ var MAX_CLOSEST_DIFF_REGION_LINES = 80;
10206
+ var MAX_CLOSEST_DIFF_LINES = 30;
9717
10207
  var RELAXED_MATCH_WORK_BUDGET = 1e6;
9718
10208
  function normalizeForFuzzyMatch(text) {
9719
10209
  return buildFuzzyIndexMap(text).normalized;
@@ -9944,6 +10434,9 @@ function reindentNewText(newText, qLines, fileLines, window) {
9944
10434
  nIdx.push(i);
9945
10435
  }
9946
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
+ }
9947
10440
  const counterpart = /* @__PURE__ */ new Map();
9948
10441
  let p = 0;
9949
10442
  while (p < qIdx.length && p < nIdx.length && qLines[qIdx[p]].trim() === newLines[nIdx[p]].trim()) {
@@ -10061,7 +10554,8 @@ function describeClosestRegion(content, oldText) {
10061
10554
  let matched = 0;
10062
10555
  let fi = i;
10063
10556
  let firstDiff;
10064
- for (let qi = firstNonBlank; qi < qLines.length; qi++) {
10557
+ let qi = firstNonBlank;
10558
+ for (; qi < qLines.length; qi++) {
10065
10559
  budget -= 1;
10066
10560
  if (budget < 0) {
10067
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.`;
@@ -10070,24 +10564,102 @@ function describeClosestRegion(content, oldText) {
10070
10564
  if (!q) continue;
10071
10565
  while (fi < fileLines.length && !fileLines[fi].trim()) fi++;
10072
10566
  if (fi >= fileLines.length || fileLines[fi].trim() !== q) {
10073
- 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 };
10074
10568
  break;
10075
10569
  }
10076
10570
  matched++;
10077
10571
  fi++;
10078
10572
  }
10079
- if (!best || matched > best.matched) best = { start: i, matched, firstDiff };
10573
+ if (!best || matched > best.matched) best = { start: i, walkEnd: fi, matched, firstDiff };
10080
10574
  }
10081
10575
  const total = qLines.filter((line) => line.trim() !== "").length;
10082
10576
  if (!best) {
10083
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.`;
10084
10578
  }
10085
- 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;
10086
10584
  if (best.firstDiff) {
10087
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));
10088
10648
  } else {
10089
10649
  parts.push("All non-empty lines match by trimmed content \u2014 the mismatch is leading whitespace or blank-line placement.");
10090
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
+ }
10091
10663
  return parts.join(" ");
10092
10664
  }
10093
10665
  function generateDiffString(oldContent, newContent, contextLines = 4) {
@@ -16743,6 +17315,9 @@ export {
16743
17315
  registerInteractionHandler,
16744
17316
  hasInteractionHandler,
16745
17317
  requestInteraction,
17318
+ AttentionCoordinator,
17319
+ interactionAttentionReason,
17320
+ interactionAttentionResolution,
16746
17321
  AGENTS_CONTEXT_FILE_NAME,
16747
17322
  buildProjectContextSection,
16748
17323
  attachAgentEventsToSessionTrace,