@gajae-code/coding-agent 0.15.2 → 0.15.4

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 (107) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/dist/types/config/autorouting-tier-map.d.ts +0 -9
  3. package/dist/types/config/file-lock.d.ts +7 -1
  4. package/dist/types/config/keybindings.d.ts +13 -3
  5. package/dist/types/config/model-registry.d.ts +5 -0
  6. package/dist/types/config/model-resolver.d.ts +1 -0
  7. package/dist/types/config/settings-schema.d.ts +65 -1
  8. package/dist/types/coordinator/contract.d.ts +1 -1
  9. package/dist/types/coordinator-mcp/question-state.d.ts +42 -2
  10. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  11. package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
  12. package/dist/types/gjc-runtime/workflow-placeholder.d.ts +6 -0
  13. package/dist/types/main.d.ts +1 -1
  14. package/dist/types/modes/action-registry.d.ts +1 -0
  15. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  16. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -1
  17. package/dist/types/modes/components/welcome.d.ts +2 -0
  18. package/dist/types/modes/controllers/input-controller.d.ts +16 -1
  19. package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +42 -0
  20. package/dist/types/modes/interactive-mode.d.ts +7 -0
  21. package/dist/types/modes/shared/oauth-url-copy.d.ts +9 -0
  22. package/dist/types/modes/types.d.ts +3 -0
  23. package/dist/types/modes/ui-language.d.ts +2 -1
  24. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  25. package/dist/types/sdk/broker/session-index.d.ts +18 -0
  26. package/dist/types/sdk/bus/index.d.ts +4 -1
  27. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
  28. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  29. package/dist/types/sdk/cli/session-cli.d.ts +1 -1
  30. package/dist/types/sdk/lifecycle/service.d.ts +32 -5
  31. package/dist/types/sdk/session.d.ts +5 -0
  32. package/dist/types/session/agent-session.d.ts +4 -0
  33. package/dist/types/session/fallback-chain-controller.d.ts +8 -0
  34. package/dist/types/slash-commands/helpers/aside.d.ts +54 -0
  35. package/dist/types/tools/ask.d.ts +9 -0
  36. package/dist/types/utils/clipboard.d.ts +9 -1
  37. package/package.json +7 -7
  38. package/scripts/generate-sdk-adapter-parity-manifest.ts +5 -2
  39. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  40. package/src/commands/sdk.ts +2 -2
  41. package/src/config/autorouting-tier-map.ts +0 -9
  42. package/src/config/file-lock.ts +15 -1
  43. package/src/config/keybindings.ts +13 -3
  44. package/src/config/model-registry.ts +132 -21
  45. package/src/config/model-resolver.ts +2 -1
  46. package/src/config/models-config-schema.ts +2 -2
  47. package/src/config/settings-schema.ts +66 -1
  48. package/src/config/settings.ts +1 -1
  49. package/src/coordinator/contract.ts +1 -0
  50. package/src/coordinator-mcp/question-state.ts +235 -4
  51. package/src/coordinator-mcp/server.ts +402 -2
  52. package/src/dap/client.ts +19 -8
  53. package/src/deep-interview/render-middleware.ts +6 -2
  54. package/src/extensibility/extensions/types.ts +2 -0
  55. package/src/gjc-runtime/deep-interview-stage.ts +20 -1
  56. package/src/gjc-runtime/deep-interview-state.ts +3 -1
  57. package/src/gjc-runtime/ralplan-runtime.ts +6 -1
  58. package/src/gjc-runtime/session-state-lock.ts +11 -2
  59. package/src/gjc-runtime/ultragoal-runtime.ts +5 -0
  60. package/src/gjc-runtime/workflow-placeholder.ts +33 -0
  61. package/src/internal-urls/docs-index.generated.ts +11 -9
  62. package/src/lsp/client.ts +39 -4
  63. package/src/main.ts +42 -11
  64. package/src/modes/action-registry.ts +7 -0
  65. package/src/modes/components/hook-selector.ts +2 -1
  66. package/src/modes/components/iterm-pet-transport.ts +4 -2
  67. package/src/modes/components/runtime-mcp-add-wizard.ts +38 -1
  68. package/src/modes/components/settings-selector.ts +1 -0
  69. package/src/modes/components/tool-status-header.ts +149 -41
  70. package/src/modes/components/welcome.ts +8 -0
  71. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  72. package/src/modes/controllers/goal-mode-controller.ts +7 -2
  73. package/src/modes/controllers/input-controller.ts +141 -7
  74. package/src/modes/controllers/plan-mode-controller.ts +12 -4
  75. package/src/modes/controllers/runtime-mcp-command-controller.ts +164 -20
  76. package/src/modes/controllers/selector-controller.ts +11 -2
  77. package/src/modes/interactive-mode.ts +88 -1
  78. package/src/modes/shared/oauth-url-copy.ts +33 -0
  79. package/src/modes/types.ts +3 -0
  80. package/src/modes/ui-language.ts +44 -3
  81. package/src/runtime-mcp/oauth-flow.ts +19 -8
  82. package/src/runtime-mcp/transports/stdio.ts +27 -13
  83. package/src/sdk/acp/adapter.ts +29 -2
  84. package/src/sdk/broker/broker.ts +26 -6
  85. package/src/sdk/broker/lifecycle-ledger.ts +28 -1
  86. package/src/sdk/broker/lifecycle.ts +664 -5
  87. package/src/sdk/broker/session-index.ts +246 -0
  88. package/src/sdk/broker/transport.ts +1 -0
  89. package/src/sdk/bus/index.ts +135 -38
  90. package/src/sdk/bus/natives-augment.d.ts +20 -1
  91. package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
  92. package/src/sdk/bus/telegram-daemon.ts +3 -0
  93. package/src/sdk/cli/session-cli.ts +34 -7
  94. package/src/sdk/lifecycle/service.ts +166 -11
  95. package/src/sdk/mcp/server.ts +2 -1
  96. package/src/sdk/protocol/operation-inventory.generated.json +40 -0
  97. package/src/sdk/protocol/operation-registry.ts +2 -1
  98. package/src/sdk/session.ts +91 -7
  99. package/src/session/agent-session.ts +473 -66
  100. package/src/session/fallback-chain-controller.ts +17 -0
  101. package/src/setup/frictionless-onboarding.ts +4 -4
  102. package/src/slash-commands/aside-command.test.ts +366 -0
  103. package/src/slash-commands/builtin-registry.ts +19 -0
  104. package/src/slash-commands/helpers/aside.ts +299 -0
  105. package/src/tools/ask-contract.ts +54 -3
  106. package/src/tools/ask.ts +54 -6
  107. package/src/utils/clipboard.ts +37 -28
@@ -20,6 +20,7 @@ import * as path from "node:path";
20
20
  import { scheduler } from "node:timers/promises";
21
21
  import * as util from "node:util";
22
22
  import {
23
+ AdaptiveCompactionTracker,
23
24
  type AfterToolCallContext,
24
25
  type AfterToolCallResult,
25
26
  type Agent,
@@ -900,6 +901,8 @@ export interface AgentSessionConfig {
900
901
  initialMCPToolSelectionIsExplicit?: boolean;
901
902
  /** Whether a discoverable built-in selection was explicitly supplied to the constructor, including an empty selection. */
902
903
  initialDiscoveredBuiltinToolSelectionIsExplicit?: boolean;
904
+ /** Whether the caller explicitly disabled every built-in tool for this session. */
905
+ explicitEmptyToolSelection?: boolean;
903
906
 
904
907
  /** Whether constructor-provided MCP selections should be persisted immediately. */
905
908
  persistInitialMCPToolSelection?: boolean;
@@ -2142,7 +2145,8 @@ const AGENT_CONTINUE_BUSY_MAX_RESCHEDULES = 50;
2142
2145
  * in-loop resample budget, so without this cap a deterministic escaper loops
2143
2146
  * forever: the fallback chain never sees a charge to exhaust on. Matching the
2144
2147
  * agent loop's own per-loop budget (MAX_ESCAPED_NONASCII_RESAMPLES + 1 wire
2145
- * attempts) keeps one steering retry plus one blind retry before the run ends.
2148
+ * attempts) gives every eligible model two steered retries before one policy
2149
+ * charge advances the chain.
2146
2150
  */
2147
2151
  const MAX_ESCAPED_NONASCII_MANAGED_RETRIES = 2;
2148
2152
 
@@ -2427,6 +2431,8 @@ export class AgentSession {
2427
2431
  // required to re-trigger. A content signature (not object identity) is used so
2428
2432
  // it survives the message-array rebuild that compaction/prune perform.
2429
2433
  #lastMidRunMaintenanceAnchorSignature: string | undefined = undefined;
2434
+ #adaptiveCompactionTracker = new AdaptiveCompactionTracker();
2435
+ #adaptiveCompactionRecordedMessageKey: string | undefined;
2430
2436
  #resourceSampler: () => EmergencyCompactionSample = () => this.#defaultResourceSample();
2431
2437
  #retainedMemorySampler: (() => RetainedMemorySample) | undefined;
2432
2438
 
@@ -2481,8 +2487,11 @@ export class AgentSession {
2481
2487
  #retryPromise: Promise<void> | undefined = undefined;
2482
2488
  #retryResolve: (() => void) | undefined = undefined;
2483
2489
  #defaultFallbackController: FallbackChainController | undefined;
2490
+ #fallbackTransitionGeneration = 0;
2484
2491
  /** Managed escaped-non-ASCII retries issued for the current logical run. Bounded so a deterministic escaper cannot loop forever through un-charged fallback retries. */
2485
2492
  #escapedNonAsciiManagedRetries = 0;
2493
+ /** Concrete models that already exhausted escaped-argument recovery this turn. */
2494
+ #escapedNonAsciiExhaustedModelKeys = new Set<string>();
2486
2495
  #overflowMaintenanceAttempts = 0;
2487
2496
  #defaultFallbackExhaustedLastTurn = false;
2488
2497
  #fallbackInvocationId = 0;
@@ -2660,6 +2669,7 @@ export class AgentSession {
2660
2669
  /** Constructor authority applies only while this AgentSession instance remains alive. */
2661
2670
  #constructorMCPToolSelection: string[] | undefined;
2662
2671
  #constructorDiscoveredBuiltinToolSelection: string[] | undefined;
2672
+ #explicitEmptyToolSelection = false;
2663
2673
  #recoveryHydrationContext: RecoveryHydrationContext | undefined;
2664
2674
  #memoryGuardClaimsLease: MemoryGuardClaimsLease | undefined;
2665
2675
 
@@ -3827,6 +3837,7 @@ export class AgentSession {
3827
3837
  this.#networkPrewarmService = config.networkPrewarmService;
3828
3838
  this.#onWorkspaceTreeReady = config.onWorkspaceTreeReady;
3829
3839
  this.#mcpDiscoveryEnabled = config.mcpDiscoveryEnabled ?? false;
3840
+ this.#explicitEmptyToolSelection = config.explicitEmptyToolSelection === true;
3830
3841
  const configuredDiscoveryMode = config.settings.get("tools.discoveryMode");
3831
3842
  this.#discoveryMode =
3832
3843
  config.discoveryMode ??
@@ -6373,6 +6384,8 @@ export class AgentSession {
6373
6384
  releasePredecessor();
6374
6385
  if (startsQueuedSuccessor) {
6375
6386
  this.#defaultFallbackChain().resetAttemptBudget();
6387
+ this.#escapedNonAsciiManagedRetries = 0;
6388
+ this.#escapedNonAsciiExhaustedModelKeys.clear();
6376
6389
  this.#overflowMaintenanceAttempts = 0;
6377
6390
  }
6378
6391
  },
@@ -7692,6 +7705,7 @@ export class AgentSession {
7692
7705
  * such event.
7693
7706
  */
7694
7707
  #syncAgentSessionId(sessionId?: string): void {
7708
+ this.#resetAdaptiveCompactionState();
7695
7709
  this.#reasoningControlContextGeneration++;
7696
7710
  const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
7697
7711
  this.agent.sessionId = sid;
@@ -8271,6 +8285,11 @@ export class AgentSession {
8271
8285
  return this.agent.state.tools.map(t => t.name);
8272
8286
  }
8273
8287
 
8288
+ /** Whether this session explicitly requested zero built-in tools. */
8289
+ isExplicitEmptyToolSelection(): boolean {
8290
+ return this.#explicitEmptyToolSelection;
8291
+ }
8292
+
8274
8293
  /** Whether the edit tool is registered in this session. */
8275
8294
  get hasEditTool(): boolean {
8276
8295
  return this.#toolRegistry.has("edit");
@@ -8902,9 +8921,11 @@ export class AgentSession {
8902
8921
  ? this.#filterSelectableMCPToolNames(sessionContext.selectedMCPToolNames)
8903
8922
  : (constructorMCPToolNames ?? this.#getConfiguredDefaultSelectedMCPToolNames());
8904
8923
  const constructorDiscoveredBuiltinToolNames = this.#resolveConstructorDiscoveredBuiltinToolSelection();
8905
- const restoredDiscoveredBuiltinToolNames = sessionContext.hasPersistedDiscoveredBuiltinToolSelection
8906
- ? this.#selectRestorableDiscoveredBuiltinToolNames(sessionContext.selectedDiscoveredBuiltinToolNames ?? [])
8907
- : (constructorDiscoveredBuiltinToolNames ?? []);
8924
+ const restoredDiscoveredBuiltinToolNames = this.#explicitEmptyToolSelection
8925
+ ? []
8926
+ : sessionContext.hasPersistedDiscoveredBuiltinToolSelection
8927
+ ? this.#selectRestorableDiscoveredBuiltinToolNames(sessionContext.selectedDiscoveredBuiltinToolNames ?? [])
8928
+ : (constructorDiscoveredBuiltinToolNames ?? []);
8908
8929
  this.#selectedDiscoveredToolNames = new Set(restoredDiscoveredBuiltinToolNames);
8909
8930
  await this.#applyActiveToolsByName(
8910
8931
  [...nextActiveNonMCPToolNames, ...restoredMCPToolNames, ...restoredDiscoveredBuiltinToolNames],
@@ -9845,6 +9866,7 @@ export class AgentSession {
9845
9866
  }
9846
9867
 
9847
9868
  async #attachAskToolIfWorkflowActive(): Promise<void> {
9869
+ if (this.#explicitEmptyToolSelection) return;
9848
9870
  const sessionId = this.sessionManager.getSessionId();
9849
9871
  const inMemoryActiveSkill =
9850
9872
  this.#activeSkillState && (!this.#activeSkillState.sessionId || this.#activeSkillState.sessionId === sessionId)
@@ -9877,6 +9899,7 @@ export class AgentSession {
9877
9899
  }
9878
9900
 
9879
9901
  #attachAskTool(): void {
9902
+ if (this.#explicitEmptyToolSelection) return;
9880
9903
  const askTool = this.#toolRegistry.get("ask");
9881
9904
  if (!askTool || this.getActiveToolNames().includes(askTool.name)) return;
9882
9905
  this.#setGuardedAgentTools([...this.agent.state.tools, askTool]);
@@ -9959,6 +9982,7 @@ export class AgentSession {
9959
9982
  }
9960
9983
 
9961
9984
  async #activatePendingGjcGoalModeRequest(): Promise<boolean> {
9985
+ if (this.#explicitEmptyToolSelection) return false;
9962
9986
  if (!this.settings.get("goal.enabled")) return false;
9963
9987
  const pendingGoal = await consumePendingGoalModeRequest(
9964
9988
  this.sessionManager.getCwd(),
@@ -13850,9 +13874,13 @@ export class AgentSession {
13850
13874
  role,
13851
13875
  this.#formatRoleModelValue(role, model, options?.selector, options?.thinkingLevel),
13852
13876
  );
13853
- if (role === "default") {
13877
+ // Only an explicit user selection starts a new fallback epoch. Internal
13878
+ // fallback switches must preserve the exhausted-model set while advancing.
13879
+ if (role === "default" && (options?.cause ?? "user-selection") === "user-selection") {
13880
+ this.#fallbackTransitionGeneration++;
13854
13881
  this.#defaultFallbackController = undefined;
13855
13882
  this.#defaultFallbackExhaustedLastTurn = false;
13883
+ this.#escapedNonAsciiExhaustedModelKeys.clear();
13856
13884
  }
13857
13885
  this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
13858
13886
 
@@ -13906,7 +13934,12 @@ export class AgentSession {
13906
13934
  ? this.#modelRegistry.getModelProfile?.(this.#activeModelProfile)
13907
13935
  : undefined,
13908
13936
  });
13909
- if (eagerTasks && this.#toolRegistry.has("task") && !this.getActiveToolNames().includes("task")) {
13937
+ if (
13938
+ !this.#explicitEmptyToolSelection &&
13939
+ eagerTasks &&
13940
+ this.#toolRegistry.has("task") &&
13941
+ !this.getActiveToolNames().includes("task")
13942
+ ) {
13910
13943
  await this.activateDiscoveredTools(["task"]);
13911
13944
  }
13912
13945
  await this.refreshBaseSystemPrompt();
@@ -15828,6 +15861,7 @@ export class AgentSession {
15828
15861
  fromExtension,
15829
15862
  preserveData,
15830
15863
  );
15864
+ this.#recordAdaptiveCompactionReset(tokensBefore);
15831
15865
  await this.#applyCompactionPostAppend(compactionEntryId, firstKeptEntryId, fromExtension);
15832
15866
 
15833
15867
  const compactionResult: CompactionResult = {
@@ -16351,7 +16385,12 @@ export class AgentSession {
16351
16385
  // which breaks the provider prompt-cache prefix mid-epoch. Only prune at a
16352
16386
  // sanctioned maintenance boundary, i.e. when the un-pruned context already
16353
16387
  // crosses the compaction threshold. Pruning may then avert full compaction.
16354
- if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens))
16388
+ const adaptiveCompactionSettings = this.#recordAdaptiveCompactionCall(
16389
+ contextTokens,
16390
+ compactionSettings,
16391
+ assistantMessage,
16392
+ );
16393
+ if (!shouldCompact(contextTokens, contextWindow, adaptiveCompactionSettings, autoCompactionOutputReserveTokens))
16355
16394
  return true;
16356
16395
  const pruneEstimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
16357
16396
  relaxedMinimum: 0,
@@ -16362,7 +16401,7 @@ export class AgentSession {
16362
16401
  !shouldCompact(
16363
16402
  Math.max(0, contextTokens - pruneEstimate.tokensSaved),
16364
16403
  contextWindow,
16365
- compactionSettings,
16404
+ adaptiveCompactionSettings,
16366
16405
  autoCompactionOutputReserveTokens,
16367
16406
  )
16368
16407
  ) {
@@ -16370,7 +16409,8 @@ export class AgentSession {
16370
16409
  if (ownershipSignal?.aborted) return false;
16371
16410
  if (pruneResult) contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
16372
16411
  }
16373
- if (shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
16412
+ const prunedCompactionSettings = this.#compactionSettingsWithAdaptiveState(compactionSettings);
16413
+ if (shouldCompact(contextTokens, contextWindow, prunedCompactionSettings, autoCompactionOutputReserveTokens)) {
16374
16414
  // Try promotion first — if a larger model is available, switch instead of compacting
16375
16415
  const promoted = await this.#tryContextPromotion(assistantMessage, ownershipSignal);
16376
16416
  if (ownershipSignal?.aborted) return false;
@@ -16424,7 +16464,7 @@ export class AgentSession {
16424
16464
  }
16425
16465
  if (isAborted()) return result("aborted");
16426
16466
 
16427
- // In-place context-full maintenance only. "off" defers entirely; "handoff"
16467
+ // context-full maintenance. "off" defers entirely; "handoff"
16428
16468
  // keeps its existing agent_end / pre-prompt boundaries (a mid-tool-loop
16429
16469
  // session swap would be far more disruptive than the overflow it avoids).
16430
16470
  const compactionSettings = this.settings.getGroup("compaction");
@@ -16440,7 +16480,12 @@ export class AgentSession {
16440
16480
  const autoCompactionOutputReserveTokens = 0;
16441
16481
  const anchor = this.#findMidRunUsageAnchor(context.messages);
16442
16482
  let contextTokens = this.#estimateMidRunContextTokens(context.messages);
16443
- if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
16483
+ const adaptiveCompactionSettings = anchor
16484
+ ? this.#recordAdaptiveCompactionCall(contextTokens, compactionSettings, anchor.message)
16485
+ : this.#compactionSettingsWithAdaptiveState(compactionSettings);
16486
+ if (
16487
+ !shouldCompact(contextTokens, contextWindow, adaptiveCompactionSettings, autoCompactionOutputReserveTokens)
16488
+ ) {
16444
16489
  return result("not-needed");
16445
16490
  }
16446
16491
  // Anti-loop (#1662): a given provider response anchors at most one
@@ -16474,7 +16519,7 @@ export class AgentSession {
16474
16519
  !shouldCompact(
16475
16520
  Math.max(0, contextTokens - pruneEstimate.tokensSaved),
16476
16521
  contextWindow,
16477
- compactionSettings,
16522
+ adaptiveCompactionSettings,
16478
16523
  autoCompactionOutputReserveTokens,
16479
16524
  )
16480
16525
  ) {
@@ -16483,7 +16528,9 @@ export class AgentSession {
16483
16528
  if (pruneResult?.failure === "artifact_persistence") return result("failed");
16484
16529
  if (pruneResult) contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
16485
16530
  }
16486
- if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
16531
+ if (
16532
+ !shouldCompact(contextTokens, contextWindow, adaptiveCompactionSettings, autoCompactionOutputReserveTokens)
16533
+ ) {
16487
16534
  return pruneResult?.committed ? result("pruned", true) : result("not-needed");
16488
16535
  }
16489
16536
 
@@ -16646,7 +16693,8 @@ export class AgentSession {
16646
16693
  // Model maxTokens is a capability ceiling, not a per-turn reservation.
16647
16694
  // Auto maintenance should track actual context fullness.
16648
16695
  const autoCompactionOutputReserveTokens = 0;
16649
- if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
16696
+ const adaptiveCompactionSettings = this.#compactionSettingsWithAdaptiveState(compactionSettings);
16697
+ if (!shouldCompact(contextTokens, contextWindow, adaptiveCompactionSettings, autoCompactionOutputReserveTokens)) {
16650
16698
  // Below the compaction threshold: optionally run evidence-gated maintenance
16651
16699
  // pruning (opt-in, high savings + cache-epoch payback required).
16652
16700
  await this.#maybeRunBelowThresholdMaintenancePrune();
@@ -16662,14 +16710,15 @@ export class AgentSession {
16662
16710
  !shouldCompact(
16663
16711
  Math.max(0, contextTokens - pruneEstimate.tokensSaved),
16664
16712
  contextWindow,
16665
- compactionSettings,
16713
+ adaptiveCompactionSettings,
16666
16714
  autoCompactionOutputReserveTokens,
16667
16715
  )
16668
16716
  ) {
16669
16717
  const pruneResult = await this.#pruneToolOutputs(undefined, true);
16670
16718
  if (pruneResult) contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
16671
16719
  }
16672
- if (shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
16720
+ const prunedCompactionSettings = this.#compactionSettingsWithAdaptiveState(compactionSettings);
16721
+ if (shouldCompact(contextTokens, contextWindow, prunedCompactionSettings, autoCompactionOutputReserveTokens)) {
16673
16722
  await this.#runAutoCompaction("threshold", false, false, {
16674
16723
  continueAfterMaintenance: false,
16675
16724
  deferHandoffMaintenance: false,
@@ -16677,6 +16726,45 @@ export class AgentSession {
16677
16726
  }
16678
16727
  }
16679
16728
 
16729
+ #recordAdaptiveCompactionCall<T extends { adaptive?: { enabled: boolean; turnWindow: number } }>(
16730
+ contextTokens: number,
16731
+ settings: T,
16732
+ assistantMessage: AssistantMessage,
16733
+ ): T {
16734
+ if (!settings.adaptive?.enabled) return settings;
16735
+ this.#adaptiveCompactionTracker.setWindowMs(settings.adaptive.turnWindow * 60_000);
16736
+ const messageKey = this.#adaptiveCompactionMessageKey(assistantMessage);
16737
+ if (this.#adaptiveCompactionRecordedMessageKey === messageKey) {
16738
+ return this.#compactionSettingsWithAdaptiveState(settings);
16739
+ }
16740
+ this.#adaptiveCompactionRecordedMessageKey = messageKey;
16741
+ this.#adaptiveCompactionTracker.recordCall(contextTokens);
16742
+ return this.#compactionSettingsWithAdaptiveState(settings);
16743
+ }
16744
+
16745
+ #adaptiveCompactionMessageKey(message: AssistantMessage): string {
16746
+ return `${message.provider}\u0000${message.model}\u0000${message.timestamp}\u0000${message.usage?.totalTokens ?? ""}`;
16747
+ }
16748
+
16749
+ #compactionSettingsWithAdaptiveState<T extends { adaptive?: { enabled: boolean } }>(settings: T): T {
16750
+ if (!settings.adaptive?.enabled) return settings;
16751
+ return {
16752
+ ...settings,
16753
+ adaptiveState: this.#adaptiveCompactionTracker.decisionState(),
16754
+ };
16755
+ }
16756
+
16757
+ #resetAdaptiveCompactionState(): void {
16758
+ this.#adaptiveCompactionTracker.reset();
16759
+ this.#adaptiveCompactionRecordedMessageKey = undefined;
16760
+ }
16761
+
16762
+ #recordAdaptiveCompactionReset(contextTokens: number): void {
16763
+ this.#adaptiveCompactionTracker.reset();
16764
+ this.#adaptiveCompactionTracker.recordCompact(contextTokens);
16765
+ this.#adaptiveCompactionRecordedMessageKey = undefined;
16766
+ }
16767
+
16680
16768
  #assistantEndedWithSuccessfulYield(assistantMessage: AssistantMessage): boolean {
16681
16769
  const toolCallId = this.#lastSuccessfulYieldToolCallId;
16682
16770
  if (!toolCallId) return false;
@@ -18127,6 +18215,7 @@ export class AgentSession {
18127
18215
  fromExtension,
18128
18216
  preserveData,
18129
18217
  );
18218
+ this.#recordAdaptiveCompactionReset(tokensBefore);
18130
18219
  await this.#applyCompactionPostAppend(compactionEntryId, firstKeptEntryId, fromExtension);
18131
18220
  if (autoCompactionSignal.aborted) return await emitAborted();
18132
18221
 
@@ -18586,6 +18675,7 @@ export class AgentSession {
18586
18675
  },
18587
18676
  onManagedAttemptAccepted: () => {
18588
18677
  controller.resetAttemptBudget();
18678
+ this.#escapedNonAsciiManagedRetries = 0;
18589
18679
  this.#overflowMaintenanceAttempts = 0;
18590
18680
  },
18591
18681
  onManagedAttemptOutcome: outcome => this.#handleManagedAttemptOutcome(outcome),
@@ -18596,6 +18686,7 @@ export class AgentSession {
18596
18686
  // A fresh user turn gets a fresh escaped-non-ASCII retry budget: the
18597
18687
  // defect is per-turn wire luck, not a sticky model property.
18598
18688
  this.#escapedNonAsciiManagedRetries = 0;
18689
+ this.#escapedNonAsciiExhaustedModelKeys.clear();
18599
18690
  const controller = this.#defaultFallbackChain();
18600
18691
  if (this.#defaultFallbackExhaustedLastTurn) {
18601
18692
  this.#defaultFallbackExhaustedLastTurn = false;
@@ -18690,6 +18781,11 @@ export class AgentSession {
18690
18781
  }
18691
18782
 
18692
18783
  async #handleManagedAttemptOutcome(outcome: ManagedAttemptOutcome): Promise<ManagedAttemptDecision> {
18784
+ const activePromptHandle = this.activePromptHandle;
18785
+ const cancellationSignal = activePromptHandle
18786
+ ? this.#runCancellationDomains.lookup(activePromptHandle)?.signal
18787
+ : undefined;
18788
+ if (cancellationSignal?.aborted) return { type: "terminal", terminal: { stopReason: "cancelled" } };
18693
18789
  if (outcome.type === "run_terminal") {
18694
18790
  this.#defaultFallbackChain().resetAttemptBudget();
18695
18791
  return { type: "terminal", terminal: { stopReason: outcome.reason } };
@@ -18699,34 +18795,148 @@ export class AgentSession {
18699
18795
  // evidence: never charge the attempt, advance the chain, or suppress the
18700
18796
  // selector. The loop already removed the defective turn from history and
18701
18797
  // bounded its own resample budget, so this decision just re-issues the
18702
- // same request on the same model. The re-issue carries the transient
18703
- // steering instruction exactly once (when the discarded attempt did not
18704
- // already have one), so a deterministic escaper has a reason to change
18705
- // its spelling; the instruction never lands in durable history.
18798
+ // same request on the same model. Every bounded re-issue carries the
18799
+ // transient steering instruction, so a deterministic escaper has a reason
18800
+ // to change its spelling; the instruction never lands in durable history.
18706
18801
  //
18707
18802
  // The retries are un-charged by design, so a deterministic escaper
18708
18803
  // would otherwise loop forever: each continuation is a fresh loop with
18709
- // a fresh in-loop resample budget, and the fallback chain never sees a
18710
- // charge to exhaust on. Bound them per logical run and fail closed to
18711
- // the terminal exhaustion message once the budget is spent — the same
18712
- // fail-closed answer the unmanaged path gives via the per-call
18713
- // rejection. New user turns reset the budget in #resetDefaultFallbackForNewTurn.
18804
+ // a fresh in-loop resample budget. After that bounded recovery budget is
18805
+ // spent, charge the active model once and advance to the next eligible
18806
+ // fallback entry. Only the final eligible model returns the terminal
18807
+ // fail-closed answer.
18714
18808
  this.#escapedNonAsciiManagedRetries += 1;
18715
18809
  if (this.#escapedNonAsciiManagedRetries > MAX_ESCAPED_NONASCII_MANAGED_RETRIES) {
18810
+ const errorMessage =
18811
+ `Managed fallback retried the escaped non-ASCII tool-call turn ${MAX_ESCAPED_NONASCII_MANAGED_RETRIES} times without a literal-UTF-8 re-issue; ` +
18812
+ `giving up on this model so the next eligible fallback can re-issue with literal UTF-8.`;
18813
+ const controller = this.#defaultFallbackChain();
18814
+ const fallbackFrom = controller.currentSelector();
18815
+ const previousModel = this.model;
18816
+ const previousThinkingLevel = this.thinkingLevel;
18817
+ if (this.model) {
18818
+ this.#escapedNonAsciiExhaustedModelKeys.add(`${this.model.provider}\u0000${this.model.id}`);
18819
+ }
18820
+ controller.discardStartedAttempt();
18821
+ const controllerStateBeforeFailure = controller.snapshotRuntimeState();
18822
+ const advanced = controller.recordEscapedArgumentsFailure(errorMessage, false);
18823
+ this.#escapedNonAsciiManagedRetries = 0;
18824
+ const abortEpoch = this.#abortAdmissionEpoch;
18825
+ if (advanced) {
18826
+ const transitionGeneration = ++this.#fallbackTransitionGeneration;
18827
+ return {
18828
+ type: "retry",
18829
+ continuation: async ownership => {
18830
+ const restoreOwnedTransition = async () => {
18831
+ if (this.#fallbackTransitionGeneration !== transitionGeneration) return;
18832
+ this.#fallbackTransitionGeneration++;
18833
+ if (!this.agent.state.isStreaming)
18834
+ await this.#restoreDefaultFallbackTransition(
18835
+ controller,
18836
+ controllerStateBeforeFailure,
18837
+ previousModel,
18838
+ previousThinkingLevel,
18839
+ );
18840
+ };
18841
+ const continuationCancelled =
18842
+ !ownership.isCurrent() ||
18843
+ ownership.lease.signal.aborted ||
18844
+ cancellationSignal?.aborted ||
18845
+ this.#abortAdmissionEpoch !== abortEpoch ||
18846
+ this.#fallbackTransitionGeneration !== transitionGeneration;
18847
+ if (continuationCancelled) {
18848
+ await restoreOwnedTransition();
18849
+ return;
18850
+ }
18851
+ if (!controller.advance()) {
18852
+ this.#fallbackTransitionGeneration++;
18853
+ this.#defaultFallbackExhaustedLastTurn = true;
18854
+ this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
18855
+ stopReason: "exhausted",
18856
+ messages: [
18857
+ this.#managedFallbackExhaustionMessage(
18858
+ outcome.message,
18859
+ `Managed fallback exhausted every eligible model after escaped non-ASCII tool-call retries; the run fails closed. ${errorMessage}`,
18860
+ ),
18861
+ ],
18862
+ });
18863
+ return;
18864
+ }
18865
+ const switched = await this.#advanceDefaultFallback(
18866
+ controller,
18867
+ "escaped_non_ascii",
18868
+ 1,
18869
+ cancellationSignal,
18870
+ abortEpoch,
18871
+ controllerStateBeforeFailure,
18872
+ previousModel,
18873
+ previousThinkingLevel,
18874
+ { emitSwitchEvent: false },
18875
+ transitionGeneration,
18876
+ );
18877
+ if (!switched) {
18878
+ if (
18879
+ this.#abortAdmissionEpoch !== abortEpoch ||
18880
+ cancellationSignal?.aborted ||
18881
+ this.#fallbackTransitionGeneration !== transitionGeneration
18882
+ ) {
18883
+ await restoreOwnedTransition();
18884
+ return;
18885
+ }
18886
+ this.#fallbackTransitionGeneration++;
18887
+ this.#defaultFallbackExhaustedLastTurn = true;
18888
+ this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
18889
+ stopReason: "exhausted",
18890
+ messages: [
18891
+ this.#managedFallbackExhaustionMessage(
18892
+ outcome.message,
18893
+ `Managed fallback exhausted every eligible model after escaped non-ASCII tool-call retries; the run fails closed. ${errorMessage}`,
18894
+ ),
18895
+ ],
18896
+ });
18897
+ return;
18898
+ }
18899
+ const continuation = this.agent.continue({
18900
+ ...this.#managedFallbackPromptOptions(),
18901
+ transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
18902
+ });
18903
+ if (
18904
+ cancellationSignal?.aborted ||
18905
+ this.#abortAdmissionEpoch !== abortEpoch ||
18906
+ this.#fallbackTransitionGeneration !== transitionGeneration
18907
+ ) {
18908
+ await restoreOwnedTransition();
18909
+ return;
18910
+ }
18911
+ this.#emitFallbackSwitchEvent(
18912
+ controller,
18913
+ fallbackFrom,
18914
+ controller.currentSelector(),
18915
+ "escaped_non_ascii",
18916
+ 1,
18917
+ );
18918
+ await continuation;
18919
+ this.#fallbackTransitionGeneration++;
18920
+ },
18921
+ };
18922
+ }
18923
+ controller.advance();
18924
+ if (this.#abortAdmissionEpoch !== abortEpoch || cancellationSignal?.aborted)
18925
+ return { type: "terminal", terminal: { stopReason: "cancelled" } };
18926
+ this.#defaultFallbackExhaustedLastTurn = true;
18716
18927
  return this.#managedFallbackExhaustionDecision(
18717
18928
  outcome.message,
18718
- `Managed fallback retried the escaped non-ASCII tool-call turn ${MAX_ESCAPED_NONASCII_MANAGED_RETRIES} times without a literal-UTF-8 re-issue; giving up so the run fails closed instead of looping.`,
18929
+ `Managed fallback exhausted every eligible model after escaped non-ASCII tool-call retries; the run fails closed. ${errorMessage}`,
18719
18930
  );
18720
18931
  }
18721
18932
  this.#defaultFallbackChain().discardStartedAttempt();
18722
- const steering = outcome.steeringPending === true;
18723
18933
  return {
18724
18934
  type: "retry",
18725
18935
  continuation: async ownership => {
18726
18936
  if (!ownership.isCurrent() || ownership.lease.signal.aborted) return;
18727
18937
  await this.agent.continue({
18728
18938
  ...this.#managedFallbackPromptOptions(),
18729
- ...(steering ? { transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage() } : {}),
18939
+ transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
18730
18940
  });
18731
18941
  },
18732
18942
  };
@@ -18798,6 +19008,7 @@ export class AgentSession {
18798
19008
  return {
18799
19009
  ...discarded,
18800
19010
  content: [{ type: "text", text: "" }],
19011
+ providerPayload: undefined,
18801
19012
  stopReason: "error",
18802
19013
  errorMessage,
18803
19014
  timestamp: Date.now(),
@@ -18842,45 +19053,162 @@ export class AgentSession {
18842
19053
  return undefined;
18843
19054
  }
18844
19055
 
19056
+ async #restoreDefaultFallbackTransition(
19057
+ controller: FallbackChainController,
19058
+ rollbackState: FallbackChainRuntimeState | undefined,
19059
+ previousModel: Model | undefined,
19060
+ previousThinkingLevel: ThinkingLevel | undefined,
19061
+ ): Promise<void> {
19062
+ if (rollbackState) controller.restoreRuntimeState(rollbackState);
19063
+ if (previousModel && this.model !== previousModel) {
19064
+ const previousEditMode = this.#resolveActiveEditMode();
19065
+ this.#setModelAuthoritatively(previousModel, "fallback-switch");
19066
+ this.#thinkingLevel = previousThinkingLevel;
19067
+ this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
19068
+ await this.#syncEditToolModeAfterModelChange(previousEditMode);
19069
+ }
19070
+ if (previousModel && this.model === previousModel && this.thinkingLevel !== previousThinkingLevel) {
19071
+ this.#thinkingLevel = previousThinkingLevel;
19072
+ this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
19073
+ }
19074
+ }
19075
+
18845
19076
  async #advanceDefaultFallback(
18846
19077
  controller: FallbackChainController,
18847
19078
  reason: string,
18848
19079
  attemptsUsed: number,
19080
+ signal?: AbortSignal,
19081
+ abortEpoch?: number,
19082
+ rollbackState?: FallbackChainRuntimeState,
19083
+ previousModel?: Model,
19084
+ previousThinkingLevel?: ThinkingLevel,
19085
+ options?: { emitSwitchEvent?: boolean },
19086
+ transitionGeneration?: number,
18849
19087
  ): Promise<boolean> {
19088
+ const promptAbortSignal = this.#promptPreflightAbortController.signal;
19089
+ const cancellationSignal = signal ? AbortSignal.any([signal, promptAbortSignal]) : promptAbortSignal;
19090
+ const transitionStillOwned = () =>
19091
+ transitionGeneration === undefined || this.#fallbackTransitionGeneration === transitionGeneration;
19092
+ const restoreFallbackTransition = () =>
19093
+ transitionStillOwned()
19094
+ ? this.#restoreDefaultFallbackTransition(controller, rollbackState, previousModel, previousThinkingLevel)
19095
+ : Promise.resolve();
19096
+ const awaitWithCancellation = async <T>(pending: Promise<T>) => {
19097
+ const cancellation = Promise.withResolvers<"aborted">();
19098
+ const onAbort = () => cancellation.resolve("aborted");
19099
+ cancellationSignal.addEventListener("abort", onAbort, { once: true });
19100
+ try {
19101
+ return await Promise.race([
19102
+ pending.then(value => ({ kind: "resolved" as const, value })),
19103
+ cancellation.promise.then(() => ({ kind: "aborted" as const })),
19104
+ ]);
19105
+ } finally {
19106
+ cancellationSignal.removeEventListener("abort", onAbort);
19107
+ }
19108
+ };
19109
+ const rollbackCancelled = async (): Promise<boolean> => {
19110
+ await restoreFallbackTransition();
19111
+ return false;
19112
+ };
19113
+ if (
19114
+ cancellationSignal.aborted ||
19115
+ (abortEpoch !== undefined && this.#abortAdmissionEpoch !== abortEpoch) ||
19116
+ !transitionStillOwned()
19117
+ )
19118
+ return await rollbackCancelled();
18850
19119
  while (!controller.isExhausted()) {
18851
19120
  const selector = controller.currentSelector();
18852
19121
  if (!selector) return false;
18853
19122
  const profileAliasIntent = this.#persistedModelProfileAliasIntent("default");
18854
- const resolved = profileAliasIntent
18855
- ? await resolveModelChainWithAuth(
18856
- [selector],
18857
- this.#modelRegistry,
18858
- this.settings,
18859
- this.credentialSessionId,
18860
- {
18861
- managedFallback: true,
18862
- ...profileAliasIntent,
18863
- canonicalSessionId: this.agent.providerSessionId ?? this.sessionId,
19123
+ const profileResolution = profileAliasIntent
19124
+ ? await (async () => {
19125
+ const canonicalSessionId = this.agent.providerSessionId ?? this.sessionId;
19126
+ const previousCanonicalVariant = this.#modelRegistry.getSessionCanonicalVariant(canonicalSessionId);
19127
+ const restoreCanonicalVariant = () => {
19128
+ if (previousCanonicalVariant !== undefined) {
19129
+ this.#modelRegistry.restoreSessionCanonicalVariant(
19130
+ canonicalSessionId,
19131
+ previousCanonicalVariant,
19132
+ );
19133
+ } else {
19134
+ this.#modelRegistry.clearCanonicalVariant(canonicalSessionId);
19135
+ }
19136
+ };
19137
+ try {
19138
+ const pendingResolution = resolveModelChainWithAuth(
19139
+ [selector],
19140
+ this.#modelRegistry,
19141
+ this.settings,
19142
+ this.credentialSessionId,
19143
+ {
19144
+ managedFallback: true,
19145
+ ...profileAliasIntent,
19146
+ canonicalSessionId,
19147
+ credentialSessionId: this.credentialSessionId,
19148
+ signal: cancellationSignal,
19149
+ },
19150
+ );
19151
+ return await awaitWithCancellation(
19152
+ pendingResolution.finally(() => {
19153
+ if (cancellationSignal.aborted) restoreCanonicalVariant();
19154
+ }),
19155
+ );
19156
+ } catch (error) {
19157
+ if (cancellationSignal.aborted) restoreCanonicalVariant();
19158
+ await restoreFallbackTransition();
19159
+ throw error;
19160
+ }
19161
+ })()
19162
+ : undefined;
19163
+ if (profileResolution?.kind === "aborted") return await rollbackCancelled();
19164
+ const resolved =
19165
+ profileResolution?.kind === "resolved"
19166
+ ? profileResolution.value
19167
+ : resolveModelRoleValue(selector, this.#modelRegistry.getAvailable(), {
19168
+ settings: this.settings,
19169
+ matchPreferences: { usageOrder: this.settings.getStorage()?.getModelUsageOrder() },
19170
+ modelRegistry: this.#modelRegistry,
19171
+ sessionId: this.agent.providerSessionId ?? this.sessionId,
18864
19172
  credentialSessionId: this.credentialSessionId,
18865
- },
18866
- )
18867
- : resolveModelRoleValue(selector, this.#modelRegistry.getAvailable(), {
18868
- settings: this.settings,
18869
- matchPreferences: { usageOrder: this.settings.getStorage()?.getModelUsageOrder() },
18870
- modelRegistry: this.#modelRegistry,
18871
- sessionId: this.agent.providerSessionId ?? this.sessionId,
18872
- credentialSessionId: this.credentialSessionId,
18873
- });
19173
+ });
19174
+ if (
19175
+ cancellationSignal.aborted ||
19176
+ (abortEpoch !== undefined && this.#abortAdmissionEpoch !== abortEpoch) ||
19177
+ !transitionStillOwned()
19178
+ )
19179
+ return await rollbackCancelled();
18874
19180
  if (!resolved.model) {
18875
19181
  controller.onResolutionSkip("unknown_model");
18876
19182
  continue;
18877
19183
  }
19184
+ const resolvedModel = resolved.model;
19185
+ if (this.#escapedNonAsciiExhaustedModelKeys.has(`${resolved.model.provider}\u0000${resolved.model.id}`)) {
19186
+ controller.onResolutionSkip("escaped_non_ascii_model_exhausted");
19187
+ continue;
19188
+ }
18878
19189
  const managedCursorUnavailable = managedCursorFallbackUnavailableReason(resolved.model, selector);
18879
19190
  if (managedCursorUnavailable) {
18880
19191
  controller.onResolutionSkip(managedCursorUnavailable);
18881
19192
  continue;
18882
19193
  }
18883
- const key = await this.#modelRegistry.getApiKey(resolved.model, this.credentialSessionId);
19194
+ const keyResult = await (async () => {
19195
+ try {
19196
+ return await awaitWithCancellation(
19197
+ this.#modelRegistry.getApiKey(resolvedModel, this.credentialSessionId),
19198
+ );
19199
+ } catch (error) {
19200
+ await rollbackCancelled();
19201
+ throw error;
19202
+ }
19203
+ })();
19204
+ if (keyResult.kind === "aborted") return await rollbackCancelled();
19205
+ const key = keyResult.value;
19206
+ if (
19207
+ cancellationSignal.aborted ||
19208
+ (abortEpoch !== undefined && this.#abortAdmissionEpoch !== abortEpoch) ||
19209
+ !transitionStillOwned()
19210
+ )
19211
+ return await rollbackCancelled();
18884
19212
  if (!isAuthenticated(key) && key !== kNoAuth) {
18885
19213
  controller.onResolutionSkip("unauthenticated");
18886
19214
  continue;
@@ -18889,33 +19217,64 @@ export class AgentSession {
18889
19217
  controller.tried.at(-1)?.selector ?? controller.chain.entries[controller.activeIndex - 1] ?? selector;
18890
19218
  const to = selector;
18891
19219
  const previousEditMode = this.#resolveActiveEditMode();
19220
+ this.#escapedNonAsciiManagedRetries = 0;
18892
19221
  this.#setModelAuthoritatively(resolved.model, "fallback-switch");
18893
- this.setThinkingLevel(
19222
+ this.#thinkingLevel = resolveThinkingLevelForModel(
19223
+ this.model,
18894
19224
  this.#thinkingLevelForResolvedFallback(resolved.explicitThinkingLevel, resolved.thinkingLevel),
18895
19225
  );
19226
+ this.agent.setThinkingLevel(toReasoningEffort(this.#thinkingLevel));
18896
19227
  await this.#syncEditToolModeAfterModelChange(previousEditMode);
18897
- if (from !== to) {
18898
- this.#emit({
18899
- type: "model_fallback_switched",
18900
- eventId: crypto.randomUUID(),
18901
- from,
18902
- to,
18903
- reason,
18904
- role:
18905
- controller.chain.origin === "subagent"
18906
- ? (controller.chain.identity ?? controller.chain.role)
18907
- : controller.chain.role,
18908
- scope: controller.chain.origin === "subagent" ? "subagent-call" : "session",
18909
- activeIndex: controller.activeIndex,
18910
- chainLength: controller.chain.entries.length,
18911
- attemptsUsed,
18912
- });
19228
+ if (
19229
+ cancellationSignal.aborted ||
19230
+ (abortEpoch !== undefined && this.#abortAdmissionEpoch !== abortEpoch) ||
19231
+ !transitionStillOwned()
19232
+ ) {
19233
+ return await rollbackCancelled();
18913
19234
  }
19235
+ if (options?.emitSwitchEvent ?? true)
19236
+ this.#emitFallbackSwitchEvent(controller, from, to, reason, attemptsUsed);
19237
+ if (
19238
+ cancellationSignal.aborted ||
19239
+ (abortEpoch !== undefined && this.#abortAdmissionEpoch !== abortEpoch) ||
19240
+ !transitionStillOwned()
19241
+ )
19242
+ return await rollbackCancelled();
18914
19243
  return true;
18915
19244
  }
18916
19245
  return false;
18917
19246
  }
18918
19247
 
19248
+ #emitFallbackSwitchEvent(
19249
+ controller: FallbackChainController,
19250
+ from: string | undefined,
19251
+ to: string | undefined,
19252
+ reason: string,
19253
+ attemptsUsed: number,
19254
+ ): void {
19255
+ if (!from || !to || from === to) return;
19256
+ const event: Extract<AgentSessionEvent, { type: "model_fallback_switched" }> = {
19257
+ type: "model_fallback_switched",
19258
+ eventId: crypto.randomUUID(),
19259
+ from,
19260
+ to,
19261
+ reason,
19262
+ role:
19263
+ controller.chain.origin === "subagent"
19264
+ ? (controller.chain.identity ?? controller.chain.role)
19265
+ : controller.chain.role,
19266
+ scope: controller.chain.origin === "subagent" ? "subagent-call" : "session",
19267
+ activeIndex: controller.activeIndex,
19268
+ chainLength: controller.chain.entries.length,
19269
+ attemptsUsed,
19270
+ };
19271
+ if (this.#eventListeners.length === 0) {
19272
+ this.#pendingFallbackSwitches.push(event);
19273
+ } else {
19274
+ this.#emit(event);
19275
+ }
19276
+ }
19277
+
18919
19278
  #emitResolutionFallbackSwitch(controller: FallbackChainController): void {
18920
19279
  if (controller.activeIndex <= 0) return;
18921
19280
  const to = controller.currentSelector();
@@ -19292,6 +19651,12 @@ export class AgentSession {
19292
19651
  }
19293
19652
 
19294
19653
  const retry = async (ownership?: ManagedAttemptContinuationOwnership): Promise<void> => {
19654
+ const activePromptHandle = this.activePromptHandle;
19655
+ const cancellationSignal =
19656
+ ownership?.domain.signal ??
19657
+ (activePromptHandle ? this.#runCancellationDomains.lookup(activePromptHandle)?.signal : undefined);
19658
+ if (ownership && (!ownership.isCurrent() || cancellationSignal?.aborted)) return;
19659
+ const abortEpoch = this.#abortAdmissionEpoch;
19295
19660
  let quotaPoolExhausted = false;
19296
19661
  if (managedFallback && !credentialRotated && !providerRetryCeilingReached) {
19297
19662
  const mark = await this.#markFailedCredential(trigger);
@@ -19300,13 +19665,55 @@ export class AgentSession {
19300
19665
  }
19301
19666
  let advanced = outcome !== "advance";
19302
19667
  let resolutionError: unknown;
19668
+ let controllerStateBeforeAdvance: FallbackChainRuntimeState | undefined;
19669
+ let previousModel: Model | undefined;
19670
+ let previousThinkingLevel: ThinkingLevel | undefined;
19303
19671
  if (outcome === "advance") {
19672
+ controllerStateBeforeAdvance = controller.snapshotRuntimeState();
19673
+ const failedEntryIndex = controller.activeIndex - 1;
19674
+ if (failedEntryIndex >= 0 && failedEntryIndex < controller.chain.entries.length) {
19675
+ controllerStateBeforeAdvance.activeIndex = failedEntryIndex;
19676
+ controllerStateBeforeAdvance.attemptsUsed = 0;
19677
+ controllerStateBeforeAdvance.totalAttemptsUsed = Math.max(
19678
+ 0,
19679
+ controllerStateBeforeAdvance.totalAttemptsUsed - 1,
19680
+ );
19681
+ controllerStateBeforeAdvance.attemptStarted = false;
19682
+ controllerStateBeforeAdvance.exhaustedForTurn = false;
19683
+ controllerStateBeforeAdvance.tried = controllerStateBeforeAdvance.tried.slice(0, -1);
19684
+ }
19685
+ previousModel = this.model;
19686
+ previousThinkingLevel = this.thinkingLevel;
19304
19687
  try {
19305
- advanced = await this.#advanceDefaultFallback(controller, trigger.class, attemptsUsed);
19688
+ advanced = await this.#advanceDefaultFallback(
19689
+ controller,
19690
+ trigger.class,
19691
+ attemptsUsed,
19692
+ cancellationSignal,
19693
+ abortEpoch,
19694
+ controllerStateBeforeAdvance,
19695
+ previousModel,
19696
+ previousThinkingLevel,
19697
+ );
19306
19698
  } catch (error) {
19307
19699
  resolutionError = error;
19308
19700
  }
19309
19701
  }
19702
+ if (ownership && (!ownership.isCurrent() || cancellationSignal?.aborted)) {
19703
+ if (this.#abortAdmissionEpoch === abortEpoch) {
19704
+ await this.#restoreDefaultFallbackTransition(
19705
+ controller,
19706
+ controllerStateBeforeAdvance,
19707
+ previousModel,
19708
+ previousThinkingLevel,
19709
+ );
19710
+ }
19711
+ return;
19712
+ }
19713
+ if (this.#abortAdmissionEpoch !== abortEpoch) {
19714
+ controller.resetForNewTurn();
19715
+ return;
19716
+ }
19310
19717
  if (!advanced) {
19311
19718
  let errorMessage = resolutionError
19312
19719
  ? `${this.#fallbackExhaustionError(controller)}; resolution failed: ${resolutionError instanceof Error ? resolutionError.message : String(resolutionError)}`