@code-yeongyu/senpi 2026.7.20-2 → 2026.7.20

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.
Files changed (160) hide show
  1. package/CHANGELOG.md +0 -18
  2. package/dist/core/agent-session.d.ts +0 -32
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +68 -368
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/compaction/compaction.d.ts +2 -2
  7. package/dist/core/compaction/compaction.d.ts.map +1 -1
  8. package/dist/core/compaction/compaction.js +11 -39
  9. package/dist/core/compaction/compaction.js.map +1 -1
  10. package/dist/core/extensions/builtin/compaction/index.d.ts.map +1 -1
  11. package/dist/core/extensions/builtin/compaction/index.js +8 -24
  12. package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
  13. package/dist/core/extensions/builtin/goal/command-registration.d.ts +2 -2
  14. package/dist/core/extensions/builtin/goal/command-registration.d.ts.map +1 -1
  15. package/dist/core/extensions/builtin/goal/command-registration.js +4 -3
  16. package/dist/core/extensions/builtin/goal/command-registration.js.map +1 -1
  17. package/dist/core/extensions/builtin/goal/index.d.ts.map +1 -1
  18. package/dist/core/extensions/builtin/goal/index.js +27 -11
  19. package/dist/core/extensions/builtin/goal/index.js.map +1 -1
  20. package/dist/core/extensions/builtin/goal/tool-registration.d.ts +2 -2
  21. package/dist/core/extensions/builtin/goal/tool-registration.d.ts.map +1 -1
  22. package/dist/core/extensions/builtin/goal/tool-registration.js +3 -2
  23. package/dist/core/extensions/builtin/goal/tool-registration.js.map +1 -1
  24. package/dist/core/extensions/builtin/index.d.ts.map +1 -1
  25. package/dist/core/extensions/builtin/index.js +0 -2
  26. package/dist/core/extensions/builtin/index.js.map +1 -1
  27. package/dist/core/extensions/builtin/nested-agents-md/index.d.ts.map +1 -1
  28. package/dist/core/extensions/builtin/nested-agents-md/index.js +1 -4
  29. package/dist/core/extensions/builtin/nested-agents-md/index.js.map +1 -1
  30. package/dist/core/extensions/builtin/rules/index.d.ts.map +1 -1
  31. package/dist/core/extensions/builtin/rules/index.js +1 -4
  32. package/dist/core/extensions/builtin/rules/index.js.map +1 -1
  33. package/dist/core/extensions/builtin/websearch/websearch/renderers.d.ts.map +1 -1
  34. package/dist/core/extensions/builtin/websearch/websearch/renderers.js +4 -13
  35. package/dist/core/extensions/builtin/websearch/websearch/renderers.js.map +1 -1
  36. package/dist/core/extensions/builtin/websearch/websearch/search.d.ts +2 -3
  37. package/dist/core/extensions/builtin/websearch/websearch/search.d.ts.map +1 -1
  38. package/dist/core/extensions/builtin/websearch/websearch/search.js +1 -2
  39. package/dist/core/extensions/builtin/websearch/websearch/search.js.map +1 -1
  40. package/dist/core/extensions/builtin/websearch/websearch/tool.d.ts.map +1 -1
  41. package/dist/core/extensions/builtin/websearch/websearch/tool.js +1 -17
  42. package/dist/core/extensions/builtin/websearch/websearch/tool.js.map +1 -1
  43. package/dist/core/extensions/builtin/websearch/websearch/types.d.ts +0 -2
  44. package/dist/core/extensions/builtin/websearch/websearch/types.d.ts.map +1 -1
  45. package/dist/core/extensions/builtin/websearch/websearch/types.js.map +1 -1
  46. package/dist/core/extensions/runner.d.ts +0 -1
  47. package/dist/core/extensions/runner.d.ts.map +1 -1
  48. package/dist/core/extensions/runner.js +0 -31
  49. package/dist/core/extensions/runner.js.map +1 -1
  50. package/dist/core/extensions/types.d.ts +10 -67
  51. package/dist/core/extensions/types.d.ts.map +1 -1
  52. package/dist/core/extensions/types.js.map +1 -1
  53. package/dist/core/sdk.d.ts.map +1 -1
  54. package/dist/core/sdk.js +0 -1
  55. package/dist/core/sdk.js.map +1 -1
  56. package/dist/core/session-manager.d.ts +1 -6
  57. package/dist/core/session-manager.d.ts.map +1 -1
  58. package/dist/core/session-manager.js +3 -19
  59. package/dist/core/session-manager.js.map +1 -1
  60. package/dist/core/settings-manager.d.ts +0 -15
  61. package/dist/core/settings-manager.d.ts.map +1 -1
  62. package/dist/core/settings-manager.js +1 -80
  63. package/dist/core/settings-manager.js.map +1 -1
  64. package/dist/modes/interactive/components/footer.d.ts +1 -2
  65. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  66. package/dist/modes/interactive/components/footer.js +2 -21
  67. package/dist/modes/interactive/components/footer.js.map +1 -1
  68. package/dist/modes/interactive/interactive-mode.d.ts +0 -4
  69. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  70. package/dist/modes/interactive/interactive-mode.js +3 -78
  71. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  72. package/docs/extensions.md +0 -28
  73. package/docs/settings.md +0 -35
  74. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +0 -12
  75. package/node_modules/@code-yeongyu/senpi-codemode/package.json +3 -3
  76. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  77. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js +8 -76
  78. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js.map +1 -1
  79. package/node_modules/@earendil-works/pi-agent-core/dist/agent.d.ts.map +1 -1
  80. package/node_modules/@earendil-works/pi-agent-core/dist/agent.js +1 -26
  81. package/node_modules/@earendil-works/pi-agent-core/dist/agent.js.map +1 -1
  82. package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.d.ts.map +1 -1
  83. package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.js +2 -24
  84. package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.js.map +1 -1
  85. package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts +0 -5
  86. package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts.map +1 -1
  87. package/node_modules/@earendil-works/pi-agent-core/dist/types.js.map +1 -1
  88. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  89. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts.map +1 -1
  90. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +25 -68
  91. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
  92. package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.d.ts.map +1 -1
  93. package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.js +52 -62
  94. package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.js.map +1 -1
  95. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts +0 -1
  96. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts.map +1 -1
  97. package/node_modules/@earendil-works/pi-ai/dist/index.js +0 -1
  98. package/node_modules/@earendil-works/pi-ai/dist/index.js.map +1 -1
  99. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +4 -0
  100. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
  101. package/node_modules/@earendil-works/pi-ai/dist/providers/data/kimi-coding.json +1 -1
  102. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  103. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  104. package/node_modules/@earendil-works/pi-ai/dist/providers/faux.d.ts +0 -1
  105. package/node_modules/@earendil-works/pi-ai/dist/providers/faux.d.ts.map +1 -1
  106. package/node_modules/@earendil-works/pi-ai/dist/providers/faux.js +0 -1
  107. package/node_modules/@earendil-works/pi-ai/dist/providers/faux.js.map +1 -1
  108. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.models.d.ts +4 -0
  109. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.models.d.ts.map +1 -1
  110. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.models.js.map +1 -1
  111. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts +0 -15
  112. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts.map +1 -1
  113. package/node_modules/@earendil-works/pi-ai/dist/types.js.map +1 -1
  114. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts.map +1 -1
  115. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +1 -5
  116. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js.map +1 -1
  117. package/node_modules/@earendil-works/pi-ai/package.json +1 -1
  118. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  119. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  120. package/package.json +6 -6
  121. package/dist/core/extensions/builtin/goal/turn-usage.d.ts +0 -20
  122. package/dist/core/extensions/builtin/goal/turn-usage.d.ts.map +0 -1
  123. package/dist/core/extensions/builtin/goal/turn-usage.js +0 -61
  124. package/dist/core/extensions/builtin/goal/turn-usage.js.map +0 -1
  125. package/dist/core/extensions/builtin/model-fallback/index.d.ts +0 -4
  126. package/dist/core/extensions/builtin/model-fallback/index.d.ts.map +0 -1
  127. package/dist/core/extensions/builtin/model-fallback/index.js +0 -55
  128. package/dist/core/extensions/builtin/model-fallback/index.js.map +0 -1
  129. package/dist/core/extensions/builtin/model-fallback/settings.d.ts +0 -6
  130. package/dist/core/extensions/builtin/model-fallback/settings.d.ts.map +0 -1
  131. package/dist/core/extensions/builtin/model-fallback/settings.js +0 -11
  132. package/dist/core/extensions/builtin/model-fallback/settings.js.map +0 -1
  133. package/dist/core/extensions/builtin/model-fallback/ui.d.ts +0 -12
  134. package/dist/core/extensions/builtin/model-fallback/ui.d.ts.map +0 -1
  135. package/dist/core/extensions/builtin/model-fallback/ui.js +0 -72
  136. package/dist/core/extensions/builtin/model-fallback/ui.js.map +0 -1
  137. package/dist/core/retry-fallback/chains.d.ts +0 -29
  138. package/dist/core/retry-fallback/chains.d.ts.map +0 -1
  139. package/dist/core/retry-fallback/chains.js +0 -109
  140. package/dist/core/retry-fallback/chains.js.map +0 -1
  141. package/dist/core/retry-fallback/controller.d.ts +0 -77
  142. package/dist/core/retry-fallback/controller.d.ts.map +0 -1
  143. package/dist/core/retry-fallback/controller.js +0 -157
  144. package/dist/core/retry-fallback/controller.js.map +0 -1
  145. package/dist/core/retry-fallback/cooldown.d.ts +0 -21
  146. package/dist/core/retry-fallback/cooldown.d.ts.map +0 -1
  147. package/dist/core/retry-fallback/cooldown.js +0 -49
  148. package/dist/core/retry-fallback/cooldown.js.map +0 -1
  149. package/dist/core/retry-fallback/log.d.ts +0 -10
  150. package/dist/core/retry-fallback/log.d.ts.map +0 -1
  151. package/dist/core/retry-fallback/log.js +0 -128
  152. package/dist/core/retry-fallback/log.js.map +0 -1
  153. package/dist/core/retry-fallback/validate.d.ts +0 -8
  154. package/dist/core/retry-fallback/validate.d.ts.map +0 -1
  155. package/dist/core/retry-fallback/validate.js +0 -63
  156. package/dist/core/retry-fallback/validate.js.map +0 -1
  157. package/node_modules/@earendil-works/pi-ai/dist/utils/stop-details.d.ts +0 -4
  158. package/node_modules/@earendil-works/pi-ai/dist/utils/stop-details.d.ts.map +0 -1
  159. package/node_modules/@earendil-works/pi-ai/dist/utils/stop-details.js +0 -6
  160. package/node_modules/@earendil-works/pi-ai/dist/utils/stop-details.js.map +0 -1
@@ -16,8 +16,7 @@ import { randomUUID } from "node:crypto";
16
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
17
  import { basename, dirname } from "node:path";
18
18
  import { prepareAgentToolCall } from "@earendil-works/pi-agent-core";
19
- import { cleanupSessionResources, isClassifierRefusal, isContextOverflow, isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, } from "@earendil-works/pi-ai/compat";
20
- import { getAgentDir } from "../config.js";
19
+ import { cleanupSessionResources, isContextOverflow, isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, } from "@earendil-works/pi-ai/compat";
21
20
  import { getThemeByName, theme } from "../modes/interactive/theme/theme.js";
22
21
  import { stripFrontmatter } from "../utils/frontmatter.js";
23
22
  import { resolvePath } from "../utils/paths.js";
@@ -35,10 +34,6 @@ import { filterContextExcludedMessages } from "./messages.js";
35
34
  import { ModelRegistry } from "./model-registry.js";
36
35
  import { getModelNarrowingPatterns, resolveModelScope } from "./model-resolver.js";
37
36
  import { expandPromptTemplate } from "./prompt-templates.js";
38
- import { RetryFallbackController } from "./retry-fallback/controller.js";
39
- import { SelectorCooldowns } from "./retry-fallback/cooldown.js";
40
- import { createFallbackLogger } from "./retry-fallback/log.js";
41
- import { validateFallbackChains } from "./retry-fallback/validate.js";
42
37
  import { buildSessionContext, CURRENT_SESSION_VERSION, getLatestCompactionEntry, } from "./session-manager.js";
43
38
  import { generateSessionTitle, shouldSkipSessionTitle } from "./session-title-generator.js";
44
39
  import { SessionWorkBarrier } from "./session-work-barrier.js";
@@ -70,39 +65,13 @@ function withoutDeletedHeaders(headers) {
70
65
  ? Object.fromEntries(Object.entries(headers).filter((entry) => entry[1] !== null))
71
66
  : undefined;
72
67
  }
73
- /**
74
- * Human-readable rejection message paired with a `CompactionRejectionCause`.
75
- *
76
- * Kept exhaustive over the union so the compiler flags any new cause that would
77
- * otherwise reintroduce silent failures at the `compaction_end` UI seam.
78
- */
79
- function describeCompactionRejection(cause) {
80
- switch (cause) {
81
- case "would-overflow":
82
- return "Compaction rejected: the produced summary would still overflow the model context window. Reduce context (e.g. /new, drop attachments) or switch to a larger-context model.";
83
- case "cancelled-by-extension":
84
- return "Compaction rejected: cancelled by an extension.";
85
- case "circuit-breaker":
86
- return "Compaction rejected: the compaction circuit breaker is open after repeated failures. Wait for the cooldown and retry.";
87
- case "per-turn-cap":
88
- return "Compaction rejected: per-turn compaction cap reached for this turn.";
89
- }
90
- }
91
68
  class CompactionRejectedError extends Error {
92
69
  constructor(rejectionCause) {
93
- super(rejectionCause === "cancelled-by-extension"
94
- ? "Compaction cancelled"
95
- : describeCompactionRejection(rejectionCause));
70
+ super(rejectionCause === "cancelled-by-extension" ? "Compaction cancelled" : "Compaction rejected");
96
71
  this.name = "CompactionRejectedError";
97
72
  this.rejectionCause = rejectionCause;
98
73
  }
99
74
  }
100
- class RequiredCompactionError extends Error {
101
- constructor() {
102
- super("Context remains above the compaction threshold because compaction did not complete");
103
- this.name = "RequiredCompactionError";
104
- }
105
- }
106
75
  class MissingModelAccessError extends Error {
107
76
  constructor() {
108
77
  super("AgentSession requires modelRuntime or modelRegistry");
@@ -158,7 +127,6 @@ export class AgentSession {
158
127
  this._retryPromise = undefined;
159
128
  this._retryResolve = undefined;
160
129
  this._userAbortPromise = undefined;
161
- this._suppressQueuedContinuationAfterUserAbort = false;
162
130
  // Bash execution state
163
131
  this._bashAbortController = undefined;
164
132
  this._pendingBashMessages = [];
@@ -190,11 +158,6 @@ export class AgentSession {
190
158
  this.agent = config.agent;
191
159
  this.sessionManager = config.sessionManager;
192
160
  this.settingsManager = config.settingsManager;
193
- const noModelFallback = config.resourceLoader.getExtensions().runtime.flagValues.get("no-model-fallback") === true ||
194
- process.env.SENPI_NO_FALLBACK === "1";
195
- if (noModelFallback) {
196
- this.settingsManager.applyOverrides({ retry: { modelFallback: false } });
197
- }
198
161
  this._scopedModels = config.scopedModels ?? [];
199
162
  this._favoriteModels = config.favoriteModels ?? [];
200
163
  this._resourceLoader = config.resourceLoader;
@@ -206,30 +169,6 @@ export class AgentSession {
206
169
  }
207
170
  this._modelRuntime = modelRuntime;
208
171
  this._modelRegistry = config.modelRegistry ?? new ModelRegistry(modelRuntime);
209
- const fallbackLogger = createFallbackLogger(config.agentDir ?? getAgentDir());
210
- this._fallbackValidationWarnings = validateFallbackChains(this.settingsManager.getRawFallbackChains(), this._modelRegistry);
211
- for (const warning of this._fallbackValidationWarnings) {
212
- fallbackLogger.warn("validation_warning", { warning });
213
- }
214
- this._retryFallback = new RetryFallbackController({
215
- getSettings: () => this.settingsManager.getRetryFallbackSettings(),
216
- registry: this._modelRegistry,
217
- cooldowns: new SelectorCooldowns(config.fallbackNow ?? (() => Date.now())),
218
- logger: fallbackLogger,
219
- switchModel: async (model, thinking, reason) => {
220
- await this._switchActiveModel(model, {
221
- persistDefault: false,
222
- appendSessionEntry: true,
223
- entryReason: reason,
224
- emitModelSelect: reason === "fallback-revert",
225
- invalidateCompaction: false,
226
- ephemeralThinkingLevel: thinking,
227
- });
228
- },
229
- emit: (event) => this._emit(event),
230
- getCurrentSelector: () => (this.model ? { model: this.model, thinkingLevel: this.thinkingLevel } : undefined),
231
- isAuthAvailable: (provider) => this._modelRuntime.hasConfiguredAuth(provider),
232
- });
233
172
  this._extensionRunnerRef = config.extensionRunnerRef;
234
173
  this._initialActiveToolNames = config.initialActiveToolNames;
235
174
  this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
@@ -370,27 +309,8 @@ export class AgentSession {
370
309
  ? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
371
310
  : undefined);
372
311
  this.agent.prepareNextTurnWithContext = async (turn, signal) => {
373
- let messages = turn.context.messages;
374
- if (turn.toolResults.length > 0) {
375
- await this._agentEventQueue;
376
- const compacted = await this._checkCompaction(turn.message, true, "threshold");
377
- if (compacted) {
378
- messages = this.agent.state.messages.slice();
379
- }
380
- else {
381
- const settings = this.settingsManager.getCompactionSettings();
382
- const contextTokens = estimateContextTokens(filterContextExcludedMessages(this.sessionManager.buildSessionContext().messages)).tokens;
383
- if (settings.enabled && this.model && shouldCompact(contextTokens, this.model.contextWindow, settings)) {
384
- throw new RequiredCompactionError();
385
- }
386
- }
387
- }
388
- const postCompactionTurn = {
389
- ...turn,
390
- context: { ...turn.context, messages },
391
- };
392
- const previousSnapshot = await previousPrepareNextTurnWithContext?.(postCompactionTurn, signal);
393
- const previousContext = previousSnapshot?.context ?? postCompactionTurn.context;
312
+ const previousSnapshot = await previousPrepareNextTurnWithContext?.(turn, signal);
313
+ const previousContext = previousSnapshot?.context ?? turn.context;
394
314
  return {
395
315
  ...previousSnapshot,
396
316
  context: {
@@ -498,8 +418,7 @@ export class AgentSession {
498
418
  return;
499
419
  }
500
420
  const lastAssistant = this._findLastAssistantInMessages(event.messages);
501
- if (!lastAssistant ||
502
- (!this._isRetryableError(lastAssistant) && !this._isHardErrorFallbackEligible(lastAssistant))) {
421
+ if (!lastAssistant || !this._isRetryableError(lastAssistant)) {
503
422
  return;
504
423
  }
505
424
  this._retryPromise = new Promise((resolve) => {
@@ -549,21 +468,11 @@ export class AgentSession {
549
468
  return false;
550
469
  }
551
470
  const lastAssistant = this._lastAssistantMessage ?? this._findLastAssistantInMessages(messages);
552
- if (!lastAssistant) {
553
- return false;
554
- }
555
- const retryableError = this._isRetryableError(lastAssistant);
556
- if (!retryableError && this._isHardErrorFallbackEligible(lastAssistant)) {
557
- return true;
558
- }
559
- if (!retryableError) {
471
+ if (!lastAssistant || !this._isRetryableError(lastAssistant)) {
560
472
  return false;
561
473
  }
562
- if (isClassifierRefusal(lastAssistant)) {
563
- return this._retryAttempt + 1 <= settings.maxRetries && this._retryFallback.canTryFallback();
564
- }
565
474
  if (this._retryAttempt + 1 > settings.maxRetries) {
566
- return this._retryFallback.canTryFallback();
475
+ return false;
567
476
  }
568
477
  const errorMessage = lastAssistant.errorMessage || "Unknown error";
569
478
  const providerDelayMs = this._getProviderRetryDelayMs(errorMessage);
@@ -577,7 +486,6 @@ export class AgentSession {
577
486
  // This ensures the UI sees the updated queue state
578
487
  if (event.type === "message_start" && event.message.role === "user") {
579
488
  this._overflowRecoveryAttempted = false;
580
- this._retryFallback.resetTurn();
581
489
  const messageText = this._getUserMessageText(event.message);
582
490
  if (messageText) {
583
491
  // Check steering queue first
@@ -626,14 +534,6 @@ export class AgentSession {
626
534
  // Reset retry counter immediately on successful assistant response
627
535
  // This prevents accumulation across multiple LLM calls within a turn
628
536
  if (assistantMsg.stopReason !== "error" && this._retryAttempt > 0) {
629
- const fallback = this._retryFallback.activeState;
630
- if (fallback) {
631
- this._emit({
632
- type: "retry_fallback_succeeded",
633
- model: this.model ? `${this.model.provider}/${this.model.id}` : "",
634
- chainKey: fallback.chainKey,
635
- });
636
- }
637
537
  this._emit({
638
538
  type: "auto_retry_end",
639
539
  success: true,
@@ -645,27 +545,16 @@ export class AgentSession {
645
545
  }
646
546
  // Check auto-retry and auto-compaction after agent completes
647
547
  let launchedContinuation = false;
648
- const userAbortSuppressedQueuedContinuation = event.type === "agent_end" && this._suppressQueuedContinuationAfterUserAbort;
649
- if (userAbortSuppressedQueuedContinuation) {
650
- this._suppressQueuedContinuationAfterUserAbort = false;
651
- }
652
- const allowsQueuedContinuation = event.type === "agent_end" && !userAbortSuppressedQueuedContinuation
653
- ? this._agentEndAllowsQueuedContinuation(event.messages)
654
- : false;
548
+ const allowsQueuedContinuation = event.type === "agent_end" ? this._agentEndAllowsQueuedContinuation(event.messages) : false;
655
549
  if (event.type === "agent_end" && this._lastAssistantMessage) {
656
550
  const msg = this._lastAssistantMessage;
657
551
  this._lastAssistantMessage = undefined;
658
- // Retry transient failures normally and eligible hard errors only through a fallback.
659
- const retryableError = this._isRetryableError(msg);
660
- let didRetry = false;
661
- if (retryableError) {
662
- didRetry = await this._handleRetryableError(msg);
663
- }
664
- else if (this._isHardErrorFallbackEligible(msg)) {
665
- didRetry = await this._handleRetryableError(msg, { hardErrorFallback: true });
552
+ // Check for retryable errors first (overloaded, rate limit, server errors)
553
+ if (this._isRetryableError(msg)) {
554
+ const didRetry = await this._handleRetryableError(msg);
555
+ if (didRetry)
556
+ return; // Retry was initiated, don't proceed to compaction
666
557
  }
667
- if (didRetry)
668
- return; // Retry was initiated, don't proceed to compaction
669
558
  this._resolveRetry();
670
559
  launchedContinuation = await this._checkCompaction(msg);
671
560
  }
@@ -709,10 +598,6 @@ export class AgentSession {
709
598
  }
710
599
  return undefined;
711
600
  }
712
- _isAssistantFromBeforeLatestCompaction(assistantMessage) {
713
- const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
714
- return compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();
715
- }
716
601
  _replaceMessageInPlace(target, replacement) {
717
602
  // Agent-core stores the finalized message object in its state before emitting message_end.
718
603
  // SessionManager persistence happens later in _processAgentEvent() with event.message.
@@ -1168,10 +1053,6 @@ export class AgentSession {
1168
1053
  await this._waitForSettledSessionWork();
1169
1054
  throwIfCancelled();
1170
1055
  }
1171
- // Turn boundary: restore the primary model if the fallback cooldown expired.
1172
- if (!this.isStreaming) {
1173
- await this._maybeRestoreFallbackPrimary();
1174
- }
1175
1056
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
1176
1057
  const preflightResult = options?.preflightResult;
1177
1058
  const promptDisposition = options?.promptDisposition;
@@ -1249,14 +1130,7 @@ export class AgentSession {
1249
1130
  // The user's new prompt is sent below, so do not call agent.continue() here.
1250
1131
  const lastAssistant = this._findLastAssistantMessage();
1251
1132
  if (lastAssistant) {
1252
- const compacted = await this._checkCompaction(lastAssistant, false, "pre_prompt");
1253
- if (!compacted && !this._isAssistantFromBeforeLatestCompaction(lastAssistant)) {
1254
- const settings = this.settingsManager.getCompactionSettings();
1255
- const contextTokens = estimateContextTokens(filterContextExcludedMessages(this.sessionManager.buildSessionContext().messages)).tokens;
1256
- if (settings.enabled && this.model && shouldCompact(contextTokens, this.model.contextWindow, settings)) {
1257
- throw new RequiredCompactionError();
1258
- }
1259
- }
1133
+ await this._checkCompaction(lastAssistant, false, "pre_prompt");
1260
1134
  }
1261
1135
  // Build messages array (custom message if any, then user message)
1262
1136
  messages = [];
@@ -1651,9 +1525,6 @@ export class AgentSession {
1651
1525
  await this._userAbortPromise;
1652
1526
  return;
1653
1527
  }
1654
- if (this.isStreaming) {
1655
- this._suppressQueuedContinuationAfterUserAbort = true;
1656
- }
1657
1528
  const abortPromise = (async () => {
1658
1529
  this.agent.abort();
1659
1530
  await this.waitForIdle();
@@ -1721,57 +1592,20 @@ export class AgentSession {
1721
1592
  if (!(await this._modelRuntime.checkAuth(model.provider))) {
1722
1593
  throw new Error(`No API key for ${model.provider}/${model.id}`);
1723
1594
  }
1724
- // A manual model change abandons any active fallback window; if a fallback
1725
- // retry sleep is still pending, cancel it so no surprise continuation fires.
1726
- const hadActiveFallback = this._retryFallback.activeState !== undefined;
1727
- this._retryFallback.clearForManualModelChange(model);
1728
- if (hadActiveFallback && this._retryAbortController) {
1729
- this.abortRetry();
1730
- }
1731
- return await this._switchActiveModel(model, {
1732
- persistDefault: true,
1733
- appendSessionEntry: true,
1734
- emitModelSelect: true,
1735
- invalidateCompaction: true,
1736
- });
1737
- }
1738
- async _maybeRestoreFallbackPrimary() {
1739
- try {
1740
- await this._retryFallback.maybeRestorePrimary(this.settingsManager.getRetryFallbackSettings().revertPolicy);
1741
- }
1742
- catch (error) {
1743
- this._retryFallback.clear();
1744
- console.error("fallback revert failed; cleared fallback state", error);
1745
- }
1746
- }
1747
- async _switchActiveModel(model, opts) {
1748
1595
  const previousModel = this.model;
1749
- if (opts.invalidateCompaction && this._modelSelectionChangesContext(previousModel, model)) {
1596
+ const invalidatesCompaction = this._modelSelectionChangesContext(previousModel, model);
1597
+ if (invalidatesCompaction) {
1750
1598
  this._invalidateCompactionForModelSelection();
1751
1599
  }
1752
- const thinkingLevel = this._getThinkingLevelForModelSwitch(opts.ephemeralThinkingLevel);
1600
+ const thinkingLevel = this._getThinkingLevelForModelSwitch();
1753
1601
  this.agent.state.model = model;
1754
- if (opts.appendSessionEntry) {
1755
- this.sessionManager.appendModelChange(model.provider, model.id, opts.entryReason, previousModel?.provider, previousModel?.id);
1756
- }
1757
- if (opts.persistDefault) {
1758
- this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
1759
- }
1602
+ this.sessionManager.appendModelChange(model.provider, model.id);
1603
+ this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
1760
1604
  const scopedMatch = this._scopedModels.find((sm) => modelsAreEqual(sm.model, model));
1761
1605
  this._currentServiceTier = this._resolveServiceTier(model, scopedMatch?.serviceTier);
1762
- if (opts.ephemeralThinkingLevel !== undefined) {
1763
- this._applyEphemeralThinkingLevel(thinkingLevel);
1764
- }
1765
- else {
1766
- this.setThinkingLevel(thinkingLevel);
1767
- }
1768
- if (!opts.emitModelSelect)
1769
- return undefined;
1606
+ this.setThinkingLevel(thinkingLevel);
1770
1607
  return await this._emitModelSelect(model, previousModel, "set");
1771
1608
  }
1772
- _applyEphemeralThinkingLevel(level) {
1773
- this.agent.state.thinkingLevel = level;
1774
- }
1775
1609
  /**
1776
1610
  * Cycle to next/previous model.
1777
1611
  * Uses favorite models, constrained by any global model narrowing.
@@ -1803,7 +1637,6 @@ export class AgentSession {
1803
1637
  if (invalidatesCompaction) {
1804
1638
  this._invalidateCompactionForModelSelection();
1805
1639
  }
1806
- this._retryFallback.clearForManualModelChange(next.model);
1807
1640
  const thinkingLevel = this._getThinkingLevelForModelSwitch(next.thinkingLevel);
1808
1641
  this.agent.state.model = next.model;
1809
1642
  this.sessionManager.appendModelChange(next.model.provider, next.model.id);
@@ -1835,9 +1668,6 @@ export class AgentSession {
1835
1668
  // Only persist if actually changing
1836
1669
  const previousLevel = this.agent.state.thinkingLevel;
1837
1670
  const isChanging = effectiveLevel !== previousLevel;
1838
- if (isChanging) {
1839
- this._retryFallback.noteManualThinkingLevel();
1840
- }
1841
1671
  this.agent.state.thinkingLevel = effectiveLevel;
1842
1672
  if (isChanging) {
1843
1673
  this.sessionManager.appendThinkingLevelChange(effectiveLevel);
@@ -2089,7 +1919,7 @@ export class AgentSession {
2089
1919
  signal,
2090
1920
  }));
2091
1921
  if (extensionResult?.cancel) {
2092
- return await this._rejectCompaction(request, requestId, extensionResult.rejectionCause ?? "cancelled-by-extension", true, extensionResult.reason);
1922
+ return await this._rejectCompaction(request, requestId, "cancelled-by-extension", true);
2093
1923
  }
2094
1924
  if (extensionResult?.compaction) {
2095
1925
  compactionResult = extensionResult.compaction;
@@ -2098,7 +1928,7 @@ export class AgentSession {
2098
1928
  }
2099
1929
  if (!compactionResult) {
2100
1930
  const { apiKey, headers, extraBody, env } = await this._getCompactionRequestAuth(this.model);
2101
- compactionResult = await compact(preparation, this.model, apiKey, headers, request.customInstructions, signal, extraBody, this.thinkingLevel, this.agent.streamFn, env, this.agent.transformContext);
1931
+ compactionResult = await compact(preparation, this.model, apiKey, headers, request.customInstructions, signal, extraBody, this.thinkingLevel, this.agent.streamFn, env);
2102
1932
  }
2103
1933
  }
2104
1934
  if (signal.aborted) {
@@ -2161,22 +1991,11 @@ export class AgentSession {
2161
1991
  fromHook: fromExtension,
2162
1992
  };
2163
1993
  const simulatedMessages = buildSessionContext([...pathEntries, simulatedCompactionEntry], simulatedCompactionEntry.id).messages;
2164
- const contextTokens = estimateMessagesTokens(filterContextExcludedMessages(simulatedMessages));
1994
+ const contextTokens = estimateContextTokens(filterContextExcludedMessages(simulatedMessages)).tokens;
2165
1995
  const settings = this.settingsManager.getCompactionSettings();
2166
1996
  return contextTokens > this.model.contextWindow - settings.reserveTokens;
2167
1997
  }
2168
- async _rejectCompaction(request, requestId, rejectionCause, aborted, extensionReason) {
2169
- // Per plan Section 1: rejection must never be silent. The compaction_end event
2170
- // carries a non-empty human-readable errorMessage (unless the user aborted, where
2171
- // the aborted branch already renders "Compaction cancelled"). session_compact is
2172
- // also emitted with accepted:false so the compaction extension's circuit-breaker
2173
- // bookkeeping stops being dead code; other builtin session_compact handlers guard
2174
- // on event.accepted.
2175
- const trimmedExtensionReason = extensionReason?.trim();
2176
- const detailedMessage = trimmedExtensionReason
2177
- ? `Compaction rejected: ${trimmedExtensionReason}`
2178
- : describeCompactionRejection(rejectionCause);
2179
- const errorMessage = aborted && !trimmedExtensionReason ? undefined : detailedMessage;
1998
+ async _rejectCompaction(request, requestId, rejectionCause, aborted) {
2180
1999
  this._emit({
2181
2000
  type: "compaction_end",
2182
2001
  reason: request.reason,
@@ -2186,16 +2005,6 @@ export class AgentSession {
2186
2005
  requestId,
2187
2006
  accepted: false,
2188
2007
  rejectionCause,
2189
- errorMessage,
2190
- });
2191
- await this._extensionRunner.emit({
2192
- type: "session_compact",
2193
- reason: request.reason,
2194
- requestId,
2195
- accepted: false,
2196
- rejectionCause,
2197
- fromExtension: false,
2198
- willRetry: false,
2199
2008
  });
2200
2009
  return { accepted: false, requestId, rejectionCause };
2201
2010
  }
@@ -2223,7 +2032,7 @@ export class AgentSession {
2223
2032
  * @param assistantMessage The assistant message to check
2224
2033
  * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
2225
2034
  */
2226
- async _checkCompaction(assistantMessage, skipAbortedCheck = true, inlineReason) {
2035
+ async _checkCompaction(assistantMessage, skipAbortedCheck = true, requestReason) {
2227
2036
  const settings = this.settingsManager.getCompactionSettings();
2228
2037
  if (!settings.enabled)
2229
2038
  return false;
@@ -2241,7 +2050,8 @@ export class AgentSession {
2241
2050
  // compaction boundary. This prevents a stale pre-compaction usage/error
2242
2051
  // from retriggering compaction on the first prompt after compaction.
2243
2052
  const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
2244
- if (this._isAssistantFromBeforeLatestCompaction(assistantMessage)) {
2053
+ const assistantIsFromBeforeCompaction = compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();
2054
+ if (assistantIsFromBeforeCompaction) {
2245
2055
  return false;
2246
2056
  }
2247
2057
  // Case 1: Overflow - LLM returned context overflow error.
@@ -2266,7 +2076,7 @@ export class AgentSession {
2266
2076
  willRetry: false,
2267
2077
  errorMessage,
2268
2078
  });
2269
- if (inlineReason === "pre_prompt") {
2079
+ if (requestReason === "pre_prompt") {
2270
2080
  throw new Error(errorMessage);
2271
2081
  }
2272
2082
  return false;
@@ -2279,47 +2089,42 @@ export class AgentSession {
2279
2089
  this.agent.state.messages = messages.slice(0, -1);
2280
2090
  this._incrementMessageRevision();
2281
2091
  }
2282
- if (inlineReason) {
2283
- return await this._runPrePromptCompaction(assistantMessage, skipAbortedCheck, "overflow", willRetry);
2092
+ if (requestReason) {
2093
+ await this._runPrePromptCompaction(assistantMessage, skipAbortedCheck, "overflow", willRetry);
2284
2094
  }
2285
2095
  else {
2286
2096
  return await this._runAutoCompaction("overflow", willRetry);
2287
2097
  }
2098
+ return false;
2288
2099
  }
2289
2100
  // Case 2: Threshold - context is getting large
2290
2101
  // For error messages or all-zero usage messages, estimate from the last valid response.
2291
2102
  // This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage
2292
2103
  // responses can still compact and do not reset context accounting.
2293
2104
  let contextTokens;
2294
- if (inlineReason) {
2295
- const messages = filterContextExcludedMessages(this.sessionManager.buildSessionContext().messages);
2296
- contextTokens = estimateContextTokens(messages).tokens;
2105
+ const directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0;
2106
+ if (assistantMessage.stopReason === "error" || directContextTokens === 0) {
2107
+ const messages = filterContextExcludedMessages(this.agent.state.messages);
2108
+ const estimate = estimateContextTokens(messages);
2109
+ if (estimate.lastUsageIndex === null)
2110
+ return false; // No usage data at all
2111
+ // Verify the usage source is post-compaction. Kept pre-compaction messages
2112
+ // have stale usage reflecting the old (larger) context and would falsely
2113
+ // trigger compaction right after one just finished.
2114
+ const usageMsg = messages[estimate.lastUsageIndex];
2115
+ if (compactionEntry &&
2116
+ usageMsg.role === "assistant" &&
2117
+ usageMsg.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
2118
+ return false;
2119
+ }
2120
+ contextTokens = estimate.tokens;
2297
2121
  }
2298
2122
  else {
2299
- const directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0;
2300
- if (assistantMessage.stopReason !== "error" && directContextTokens !== 0) {
2301
- contextTokens = directContextTokens;
2302
- }
2303
- else {
2304
- const messages = filterContextExcludedMessages(this.agent.state.messages);
2305
- const estimate = estimateContextTokens(messages);
2306
- if (estimate.lastUsageIndex === null)
2307
- return false; // No usage data at all
2308
- // Verify the usage source is post-compaction. Kept pre-compaction messages
2309
- // have stale usage reflecting the old (larger) context and would falsely
2310
- // trigger compaction right after one just finished.
2311
- const usageMsg = messages[estimate.lastUsageIndex];
2312
- if (compactionEntry &&
2313
- usageMsg.role === "assistant" &&
2314
- usageMsg.timestamp <= new Date(compactionEntry.timestamp).getTime()) {
2315
- return false;
2316
- }
2317
- contextTokens = estimate.tokens;
2318
- }
2123
+ contextTokens = directContextTokens;
2319
2124
  }
2320
2125
  if (shouldCompact(contextTokens, contextWindow, settings)) {
2321
- if (inlineReason) {
2322
- return await this._runPrePromptCompaction(assistantMessage, skipAbortedCheck, inlineReason);
2126
+ if (requestReason) {
2127
+ await this._runPrePromptCompaction(assistantMessage, skipAbortedCheck);
2323
2128
  }
2324
2129
  else {
2325
2130
  return await this._runAutoCompaction("threshold", false);
@@ -2340,7 +2145,6 @@ export class AgentSession {
2340
2145
  if (!execution.accepted && isContextOverflow(lastAssistantMessage, this.model?.contextWindow ?? 0)) {
2341
2146
  this._overflowRecoveryAttempted = false;
2342
2147
  }
2343
- return execution.accepted;
2344
2148
  }
2345
2149
  catch (error) {
2346
2150
  if (isContextOverflow(lastAssistantMessage, this.model?.contextWindow ?? 0)) {
@@ -2356,7 +2160,6 @@ export class AgentSession {
2356
2160
  willRetry,
2357
2161
  errorMessage: aborted ? undefined : `Pre-prompt compaction failed: ${errorMessage}`,
2358
2162
  });
2359
- return false;
2360
2163
  }
2361
2164
  finally {
2362
2165
  this._compactionAbortController = undefined;
@@ -2679,37 +2482,6 @@ export class AgentSession {
2679
2482
  },
2680
2483
  getContextUsage: () => this.getContextUsage(),
2681
2484
  getCompactionSettings: () => this.settingsManager.getCompactionSettings(),
2682
- sessionSettings: {
2683
- getRetryFallbackSettings: () => this.settingsManager.getRetryFallbackSettings(),
2684
- setFallbackChain: async (key, entries) => {
2685
- this.settingsManager.setFallbackChain(key, [...entries]);
2686
- await this.settingsManager.flush();
2687
- },
2688
- removeFallbackChain: async (key) => {
2689
- this.settingsManager.removeFallbackChain(key);
2690
- await this.settingsManager.flush();
2691
- },
2692
- setModelFallbackEnabled: async (enabled) => {
2693
- this.settingsManager.setModelFallbackEnabled(enabled);
2694
- await this.settingsManager.flush();
2695
- },
2696
- setFallbackRevertPolicy: async (policy) => {
2697
- this.settingsManager.setFallbackRevertPolicy(policy);
2698
- await this.settingsManager.flush();
2699
- },
2700
- reload: () => this.settingsManager.reload(),
2701
- getFallbackStatus: () => {
2702
- const active = this._retryFallback.activeState;
2703
- if (!active)
2704
- return undefined;
2705
- return {
2706
- active: true,
2707
- currentModel: this.model ? `${this.model.provider}/${this.model.id}` : undefined,
2708
- originalSelector: active.originalSelector,
2709
- pinned: active.pinned,
2710
- };
2711
- },
2712
- },
2713
2485
  compact: (options) => {
2714
2486
  void (async () => {
2715
2487
  this._disconnectFromAgent();
@@ -2781,10 +2553,6 @@ export class AgentSession {
2781
2553
  },
2782
2554
  });
2783
2555
  }
2784
- /** Fallback-chain configuration warnings calculated when this session started. */
2785
- get fallbackValidationWarnings() {
2786
- return this._fallbackValidationWarnings;
2787
- }
2788
2556
  _refreshToolRegistry(options) {
2789
2557
  const previousRegistryNames = new Set(this._toolRegistry.keys());
2790
2558
  const previousActiveToolNames = this.getActiveToolNames();
@@ -2932,25 +2700,16 @@ export class AgentSession {
2932
2700
  * Context overflow errors are NOT retryable (handled by compaction instead).
2933
2701
  */
2934
2702
  _isRetryableError(message) {
2703
+ if (!message.errorMessage)
2704
+ return false;
2935
2705
  // Context overflow is handled by compaction, not retry.
2936
2706
  if (isContextOverflow(message, this.model?.contextWindow ?? 0))
2937
2707
  return false;
2938
- if (isClassifierRefusal(message))
2939
- return true;
2940
- if (!message.errorMessage)
2941
- return false;
2942
2708
  if (message.stopReason === "aborted") {
2943
2709
  return /timed? out|timeout/i.test(message.errorMessage);
2944
2710
  }
2945
2711
  return isRetryableAssistantError(message);
2946
2712
  }
2947
- _isHardErrorFallbackEligible(message) {
2948
- return (message.stopReason === "error" &&
2949
- !isContextOverflow(message, this.model?.contextWindow ?? 0) &&
2950
- !isClassifierRefusal(message) &&
2951
- !message.content.some((content) => content.type === "toolCall") &&
2952
- this._retryFallback.canTryFallback());
2953
- }
2954
2713
  _getProviderRetryDelayMs(errorMessage) {
2955
2714
  const retryAfterMsMatch = errorMessage.match(/\bretry[-_ ]?after[-_ ]?ms\s*[:=]\s*(\d+(?:\.\d+)?)/i);
2956
2715
  if (retryAfterMsMatch) {
@@ -2983,7 +2742,7 @@ export class AgentSession {
2983
2742
  * Handle retryable errors with exponential backoff.
2984
2743
  * @returns true if retry was initiated, false if max retries exceeded or disabled
2985
2744
  */
2986
- async _handleRetryableError(message, options = {}) {
2745
+ async _handleRetryableError(message) {
2987
2746
  const settings = this.settingsManager.getRetrySettings();
2988
2747
  if (!settings.enabled) {
2989
2748
  this._resolveRetry();
@@ -2996,71 +2755,21 @@ export class AgentSession {
2996
2755
  this._retryResolve = resolve;
2997
2756
  });
2998
2757
  }
2999
- const errorMessage = message.errorMessage || "Unknown error";
3000
- const isRefusal = isClassifierRefusal(message);
3001
- const hardErrorFallback = options.hardErrorFallback === true;
3002
- let switchedFallback = false;
3003
- if (hardErrorFallback) {
3004
- // A non-retryable provider failure must never replay on the same model.
3005
- switchedFallback = await this._retryFallback.tryFallback("hard-error", { errorMessage });
3006
- if (!switchedFallback) {
3007
- this._resolveRetry();
3008
- return false;
3009
- }
3010
- // The fallback starts fresh; the failed model's transient attempts do not carry over.
3011
- this._retryAttempt = 1;
3012
- }
3013
- else if (isRefusal) {
3014
- // Refusals are only retried through a new chain candidate. They never use
3015
- // same-model retries or the transient over-budget fallback escape hatch.
3016
- if (this._retryAttempt + 1 > settings.maxRetries) {
3017
- this._resolveRetry();
3018
- return false;
3019
- }
3020
- switchedFallback = await this._retryFallback.tryFallback("refusal", {});
3021
- if (!switchedFallback) {
3022
- const exhaustedChainKey = this._retryFallback.exhaustedChainKey;
3023
- if (exhaustedChainKey) {
3024
- this._emit({ type: "retry_fallback_exhausted", chainKey: exhaustedChainKey, lastError: errorMessage });
3025
- }
3026
- this._resolveRetry();
3027
- return false;
3028
- }
3029
- this._retryAttempt++;
3030
- }
3031
- else {
3032
- this._retryAttempt++;
3033
- if (this._retryAttempt > settings.maxRetries) {
3034
- switchedFallback = await this._retryFallback.tryFallback("transient", {
3035
- errorMessage,
3036
- retryAfterMs: this._getProviderRetryDelayMs(errorMessage),
3037
- });
3038
- if (switchedFallback) {
3039
- // The new model receives a fresh retry budget; the failed model does not.
3040
- this._retryAttempt = 1;
3041
- }
3042
- else {
3043
- const exhaustedChainKey = this._retryFallback.exhaustedChainKey;
3044
- if (exhaustedChainKey) {
3045
- this._emit({
3046
- type: "retry_fallback_exhausted",
3047
- chainKey: exhaustedChainKey,
3048
- lastError: errorMessage,
3049
- });
3050
- }
3051
- this._emit({
3052
- type: "auto_retry_end",
3053
- success: false,
3054
- attempt: this._retryAttempt - 1,
3055
- finalError: message.errorMessage,
3056
- });
3057
- this._retryAttempt = 0;
3058
- this._resolveRetry();
3059
- return false;
3060
- }
3061
- }
2758
+ this._retryAttempt++;
2759
+ if (this._retryAttempt > settings.maxRetries) {
2760
+ // Max retries exceeded, emit final failure and reset
2761
+ this._emit({
2762
+ type: "auto_retry_end",
2763
+ success: false,
2764
+ attempt: this._retryAttempt - 1,
2765
+ finalError: message.errorMessage,
2766
+ });
2767
+ this._retryAttempt = 0;
2768
+ this._resolveRetry(); // Resolve so waitForRetry() completes
2769
+ return false;
3062
2770
  }
3063
- const providerDelayMs = isRefusal || hardErrorFallback ? undefined : this._getProviderRetryDelayMs(errorMessage);
2771
+ const errorMessage = message.errorMessage || "Unknown error";
2772
+ const providerDelayMs = this._getProviderRetryDelayMs(errorMessage);
3064
2773
  const maxRetryDelayMs = this.settingsManager.getProviderRetrySettings().maxRetryDelayMs;
3065
2774
  if (providerDelayMs !== undefined && providerDelayMs > maxRetryDelayMs) {
3066
2775
  this._emit({
@@ -3073,13 +2782,7 @@ export class AgentSession {
3073
2782
  this._resolveRetry();
3074
2783
  return false;
3075
2784
  }
3076
- if (!switchedFallback) {
3077
- switchedFallback = await this._retryFallback.tryFallback("transient", {
3078
- errorMessage,
3079
- retryAfterMs: providerDelayMs,
3080
- });
3081
- }
3082
- const delayMs = switchedFallback ? 0 : (providerDelayMs ?? settings.baseDelayMs * 2 ** (this._retryAttempt - 1));
2785
+ const delayMs = providerDelayMs ?? settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
3083
2786
  // Prepare before auto_retry_start so an immediate Esc can cancel the retry sleep.
3084
2787
  this._retryAbortController = new AbortController();
3085
2788
  this._emit({
@@ -3115,9 +2818,6 @@ export class AgentSession {
3115
2818
  return false;
3116
2819
  }
3117
2820
  this._retryAbortController = undefined;
3118
- // Turn boundary: a suppression that expired during the backoff sleep lets
3119
- // the retry continue on the restored primary instead of the fallback model.
3120
- await this._maybeRestoreFallbackPrimary();
3121
2821
  // Retry via continue() - use setTimeout to break out of event handler chain
3122
2822
  setTimeout(() => {
3123
2823
  this.agent.continue().catch(() => {