@gajae-code/coding-agent 0.15.2 → 0.15.3
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.
- package/CHANGELOG.md +37 -1
- package/dist/types/config/autorouting-tier-map.d.ts +0 -9
- package/dist/types/config/file-lock.d.ts +7 -1
- package/dist/types/config/keybindings.d.ts +13 -3
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +13 -1
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +42 -2
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/modes/action-registry.d.ts +1 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +16 -1
- package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +42 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/shared/oauth-url-copy.d.ts +9 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/ui-language.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/broker/session-index.d.ts +18 -0
- package/dist/types/sdk/bus/index.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/cli/session-cli.d.ts +1 -1
- package/dist/types/sdk/lifecycle/service.d.ts +32 -5
- package/dist/types/sdk/session.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +4 -0
- package/dist/types/session/fallback-chain-controller.d.ts +8 -0
- package/dist/types/slash-commands/helpers/aside.d.ts +54 -0
- package/dist/types/tools/ask.d.ts +8 -0
- package/dist/types/utils/clipboard.d.ts +9 -1
- package/package.json +7 -7
- package/scripts/generate-sdk-adapter-parity-manifest.ts +5 -2
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/src/commands/sdk.ts +2 -2
- package/src/config/autorouting-tier-map.ts +0 -9
- package/src/config/file-lock.ts +15 -1
- package/src/config/keybindings.ts +13 -3
- package/src/config/model-registry.ts +132 -21
- package/src/config/model-resolver.ts +2 -1
- package/src/config/models-config-schema.ts +2 -2
- package/src/config/settings-schema.ts +13 -1
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +235 -4
- package/src/coordinator-mcp/server.ts +402 -2
- package/src/dap/client.ts +19 -8
- package/src/gjc-runtime/deep-interview-stage.ts +20 -1
- package/src/gjc-runtime/deep-interview-state.ts +3 -1
- package/src/gjc-runtime/session-state-lock.ts +11 -2
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/lsp/client.ts +39 -4
- package/src/main.ts +42 -11
- package/src/modes/action-registry.ts +7 -0
- package/src/modes/components/iterm-pet-transport.ts +4 -2
- package/src/modes/components/runtime-mcp-add-wizard.ts +38 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/tool-status-header.ts +149 -41
- package/src/modes/components/welcome.ts +8 -0
- package/src/modes/controllers/goal-mode-controller.ts +7 -2
- package/src/modes/controllers/input-controller.ts +141 -7
- package/src/modes/controllers/plan-mode-controller.ts +12 -4
- package/src/modes/controllers/runtime-mcp-command-controller.ts +164 -20
- package/src/modes/controllers/selector-controller.ts +11 -2
- package/src/modes/interactive-mode.ts +88 -1
- package/src/modes/shared/oauth-url-copy.ts +33 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/ui-language.ts +44 -3
- package/src/runtime-mcp/oauth-flow.ts +19 -8
- package/src/runtime-mcp/transports/stdio.ts +27 -13
- package/src/sdk/acp/adapter.ts +29 -2
- package/src/sdk/broker/broker.ts +26 -6
- package/src/sdk/broker/lifecycle-ledger.ts +28 -1
- package/src/sdk/broker/lifecycle.ts +664 -5
- package/src/sdk/broker/session-index.ts +246 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/index.ts +135 -38
- package/src/sdk/bus/natives-augment.d.ts +20 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +3 -0
- package/src/sdk/cli/session-cli.ts +34 -7
- package/src/sdk/lifecycle/service.ts +166 -11
- package/src/sdk/mcp/server.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +40 -0
- package/src/sdk/protocol/operation-registry.ts +2 -1
- package/src/sdk/session.ts +91 -7
- package/src/session/agent-session.ts +403 -56
- package/src/session/fallback-chain-controller.ts +17 -0
- package/src/setup/frictionless-onboarding.ts +4 -4
- package/src/slash-commands/aside-command.test.ts +366 -0
- package/src/slash-commands/builtin-registry.ts +19 -0
- package/src/slash-commands/helpers/aside.ts +299 -0
- package/src/tools/ask.ts +8 -0
- package/src/utils/clipboard.ts +37 -28
|
@@ -900,6 +900,8 @@ export interface AgentSessionConfig {
|
|
|
900
900
|
initialMCPToolSelectionIsExplicit?: boolean;
|
|
901
901
|
/** Whether a discoverable built-in selection was explicitly supplied to the constructor, including an empty selection. */
|
|
902
902
|
initialDiscoveredBuiltinToolSelectionIsExplicit?: boolean;
|
|
903
|
+
/** Whether the caller explicitly disabled every built-in tool for this session. */
|
|
904
|
+
explicitEmptyToolSelection?: boolean;
|
|
903
905
|
|
|
904
906
|
/** Whether constructor-provided MCP selections should be persisted immediately. */
|
|
905
907
|
persistInitialMCPToolSelection?: boolean;
|
|
@@ -2142,7 +2144,8 @@ const AGENT_CONTINUE_BUSY_MAX_RESCHEDULES = 50;
|
|
|
2142
2144
|
* in-loop resample budget, so without this cap a deterministic escaper loops
|
|
2143
2145
|
* forever: the fallback chain never sees a charge to exhaust on. Matching the
|
|
2144
2146
|
* agent loop's own per-loop budget (MAX_ESCAPED_NONASCII_RESAMPLES + 1 wire
|
|
2145
|
-
* attempts)
|
|
2147
|
+
* attempts) gives every eligible model two steered retries before one policy
|
|
2148
|
+
* charge advances the chain.
|
|
2146
2149
|
*/
|
|
2147
2150
|
const MAX_ESCAPED_NONASCII_MANAGED_RETRIES = 2;
|
|
2148
2151
|
|
|
@@ -2481,8 +2484,11 @@ export class AgentSession {
|
|
|
2481
2484
|
#retryPromise: Promise<void> | undefined = undefined;
|
|
2482
2485
|
#retryResolve: (() => void) | undefined = undefined;
|
|
2483
2486
|
#defaultFallbackController: FallbackChainController | undefined;
|
|
2487
|
+
#fallbackTransitionGeneration = 0;
|
|
2484
2488
|
/** 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
2489
|
#escapedNonAsciiManagedRetries = 0;
|
|
2490
|
+
/** Concrete models that already exhausted escaped-argument recovery this turn. */
|
|
2491
|
+
#escapedNonAsciiExhaustedModelKeys = new Set<string>();
|
|
2486
2492
|
#overflowMaintenanceAttempts = 0;
|
|
2487
2493
|
#defaultFallbackExhaustedLastTurn = false;
|
|
2488
2494
|
#fallbackInvocationId = 0;
|
|
@@ -2660,6 +2666,7 @@ export class AgentSession {
|
|
|
2660
2666
|
/** Constructor authority applies only while this AgentSession instance remains alive. */
|
|
2661
2667
|
#constructorMCPToolSelection: string[] | undefined;
|
|
2662
2668
|
#constructorDiscoveredBuiltinToolSelection: string[] | undefined;
|
|
2669
|
+
#explicitEmptyToolSelection = false;
|
|
2663
2670
|
#recoveryHydrationContext: RecoveryHydrationContext | undefined;
|
|
2664
2671
|
#memoryGuardClaimsLease: MemoryGuardClaimsLease | undefined;
|
|
2665
2672
|
|
|
@@ -3827,6 +3834,7 @@ export class AgentSession {
|
|
|
3827
3834
|
this.#networkPrewarmService = config.networkPrewarmService;
|
|
3828
3835
|
this.#onWorkspaceTreeReady = config.onWorkspaceTreeReady;
|
|
3829
3836
|
this.#mcpDiscoveryEnabled = config.mcpDiscoveryEnabled ?? false;
|
|
3837
|
+
this.#explicitEmptyToolSelection = config.explicitEmptyToolSelection === true;
|
|
3830
3838
|
const configuredDiscoveryMode = config.settings.get("tools.discoveryMode");
|
|
3831
3839
|
this.#discoveryMode =
|
|
3832
3840
|
config.discoveryMode ??
|
|
@@ -6373,6 +6381,8 @@ export class AgentSession {
|
|
|
6373
6381
|
releasePredecessor();
|
|
6374
6382
|
if (startsQueuedSuccessor) {
|
|
6375
6383
|
this.#defaultFallbackChain().resetAttemptBudget();
|
|
6384
|
+
this.#escapedNonAsciiManagedRetries = 0;
|
|
6385
|
+
this.#escapedNonAsciiExhaustedModelKeys.clear();
|
|
6376
6386
|
this.#overflowMaintenanceAttempts = 0;
|
|
6377
6387
|
}
|
|
6378
6388
|
},
|
|
@@ -8271,6 +8281,11 @@ export class AgentSession {
|
|
|
8271
8281
|
return this.agent.state.tools.map(t => t.name);
|
|
8272
8282
|
}
|
|
8273
8283
|
|
|
8284
|
+
/** Whether this session explicitly requested zero built-in tools. */
|
|
8285
|
+
isExplicitEmptyToolSelection(): boolean {
|
|
8286
|
+
return this.#explicitEmptyToolSelection;
|
|
8287
|
+
}
|
|
8288
|
+
|
|
8274
8289
|
/** Whether the edit tool is registered in this session. */
|
|
8275
8290
|
get hasEditTool(): boolean {
|
|
8276
8291
|
return this.#toolRegistry.has("edit");
|
|
@@ -8902,9 +8917,11 @@ export class AgentSession {
|
|
|
8902
8917
|
? this.#filterSelectableMCPToolNames(sessionContext.selectedMCPToolNames)
|
|
8903
8918
|
: (constructorMCPToolNames ?? this.#getConfiguredDefaultSelectedMCPToolNames());
|
|
8904
8919
|
const constructorDiscoveredBuiltinToolNames = this.#resolveConstructorDiscoveredBuiltinToolSelection();
|
|
8905
|
-
const restoredDiscoveredBuiltinToolNames =
|
|
8906
|
-
?
|
|
8907
|
-
:
|
|
8920
|
+
const restoredDiscoveredBuiltinToolNames = this.#explicitEmptyToolSelection
|
|
8921
|
+
? []
|
|
8922
|
+
: sessionContext.hasPersistedDiscoveredBuiltinToolSelection
|
|
8923
|
+
? this.#selectRestorableDiscoveredBuiltinToolNames(sessionContext.selectedDiscoveredBuiltinToolNames ?? [])
|
|
8924
|
+
: (constructorDiscoveredBuiltinToolNames ?? []);
|
|
8908
8925
|
this.#selectedDiscoveredToolNames = new Set(restoredDiscoveredBuiltinToolNames);
|
|
8909
8926
|
await this.#applyActiveToolsByName(
|
|
8910
8927
|
[...nextActiveNonMCPToolNames, ...restoredMCPToolNames, ...restoredDiscoveredBuiltinToolNames],
|
|
@@ -9845,6 +9862,7 @@ export class AgentSession {
|
|
|
9845
9862
|
}
|
|
9846
9863
|
|
|
9847
9864
|
async #attachAskToolIfWorkflowActive(): Promise<void> {
|
|
9865
|
+
if (this.#explicitEmptyToolSelection) return;
|
|
9848
9866
|
const sessionId = this.sessionManager.getSessionId();
|
|
9849
9867
|
const inMemoryActiveSkill =
|
|
9850
9868
|
this.#activeSkillState && (!this.#activeSkillState.sessionId || this.#activeSkillState.sessionId === sessionId)
|
|
@@ -9877,6 +9895,7 @@ export class AgentSession {
|
|
|
9877
9895
|
}
|
|
9878
9896
|
|
|
9879
9897
|
#attachAskTool(): void {
|
|
9898
|
+
if (this.#explicitEmptyToolSelection) return;
|
|
9880
9899
|
const askTool = this.#toolRegistry.get("ask");
|
|
9881
9900
|
if (!askTool || this.getActiveToolNames().includes(askTool.name)) return;
|
|
9882
9901
|
this.#setGuardedAgentTools([...this.agent.state.tools, askTool]);
|
|
@@ -9959,6 +9978,7 @@ export class AgentSession {
|
|
|
9959
9978
|
}
|
|
9960
9979
|
|
|
9961
9980
|
async #activatePendingGjcGoalModeRequest(): Promise<boolean> {
|
|
9981
|
+
if (this.#explicitEmptyToolSelection) return false;
|
|
9962
9982
|
if (!this.settings.get("goal.enabled")) return false;
|
|
9963
9983
|
const pendingGoal = await consumePendingGoalModeRequest(
|
|
9964
9984
|
this.sessionManager.getCwd(),
|
|
@@ -13850,9 +13870,13 @@ export class AgentSession {
|
|
|
13850
13870
|
role,
|
|
13851
13871
|
this.#formatRoleModelValue(role, model, options?.selector, options?.thinkingLevel),
|
|
13852
13872
|
);
|
|
13853
|
-
|
|
13873
|
+
// Only an explicit user selection starts a new fallback epoch. Internal
|
|
13874
|
+
// fallback switches must preserve the exhausted-model set while advancing.
|
|
13875
|
+
if (role === "default" && (options?.cause ?? "user-selection") === "user-selection") {
|
|
13876
|
+
this.#fallbackTransitionGeneration++;
|
|
13854
13877
|
this.#defaultFallbackController = undefined;
|
|
13855
13878
|
this.#defaultFallbackExhaustedLastTurn = false;
|
|
13879
|
+
this.#escapedNonAsciiExhaustedModelKeys.clear();
|
|
13856
13880
|
}
|
|
13857
13881
|
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
13858
13882
|
|
|
@@ -13906,7 +13930,12 @@ export class AgentSession {
|
|
|
13906
13930
|
? this.#modelRegistry.getModelProfile?.(this.#activeModelProfile)
|
|
13907
13931
|
: undefined,
|
|
13908
13932
|
});
|
|
13909
|
-
if (
|
|
13933
|
+
if (
|
|
13934
|
+
!this.#explicitEmptyToolSelection &&
|
|
13935
|
+
eagerTasks &&
|
|
13936
|
+
this.#toolRegistry.has("task") &&
|
|
13937
|
+
!this.getActiveToolNames().includes("task")
|
|
13938
|
+
) {
|
|
13910
13939
|
await this.activateDiscoveredTools(["task"]);
|
|
13911
13940
|
}
|
|
13912
13941
|
await this.refreshBaseSystemPrompt();
|
|
@@ -18586,6 +18615,7 @@ export class AgentSession {
|
|
|
18586
18615
|
},
|
|
18587
18616
|
onManagedAttemptAccepted: () => {
|
|
18588
18617
|
controller.resetAttemptBudget();
|
|
18618
|
+
this.#escapedNonAsciiManagedRetries = 0;
|
|
18589
18619
|
this.#overflowMaintenanceAttempts = 0;
|
|
18590
18620
|
},
|
|
18591
18621
|
onManagedAttemptOutcome: outcome => this.#handleManagedAttemptOutcome(outcome),
|
|
@@ -18596,6 +18626,7 @@ export class AgentSession {
|
|
|
18596
18626
|
// A fresh user turn gets a fresh escaped-non-ASCII retry budget: the
|
|
18597
18627
|
// defect is per-turn wire luck, not a sticky model property.
|
|
18598
18628
|
this.#escapedNonAsciiManagedRetries = 0;
|
|
18629
|
+
this.#escapedNonAsciiExhaustedModelKeys.clear();
|
|
18599
18630
|
const controller = this.#defaultFallbackChain();
|
|
18600
18631
|
if (this.#defaultFallbackExhaustedLastTurn) {
|
|
18601
18632
|
this.#defaultFallbackExhaustedLastTurn = false;
|
|
@@ -18690,6 +18721,11 @@ export class AgentSession {
|
|
|
18690
18721
|
}
|
|
18691
18722
|
|
|
18692
18723
|
async #handleManagedAttemptOutcome(outcome: ManagedAttemptOutcome): Promise<ManagedAttemptDecision> {
|
|
18724
|
+
const activePromptHandle = this.activePromptHandle;
|
|
18725
|
+
const cancellationSignal = activePromptHandle
|
|
18726
|
+
? this.#runCancellationDomains.lookup(activePromptHandle)?.signal
|
|
18727
|
+
: undefined;
|
|
18728
|
+
if (cancellationSignal?.aborted) return { type: "terminal", terminal: { stopReason: "cancelled" } };
|
|
18693
18729
|
if (outcome.type === "run_terminal") {
|
|
18694
18730
|
this.#defaultFallbackChain().resetAttemptBudget();
|
|
18695
18731
|
return { type: "terminal", terminal: { stopReason: outcome.reason } };
|
|
@@ -18699,34 +18735,148 @@ export class AgentSession {
|
|
|
18699
18735
|
// evidence: never charge the attempt, advance the chain, or suppress the
|
|
18700
18736
|
// selector. The loop already removed the defective turn from history and
|
|
18701
18737
|
// bounded its own resample budget, so this decision just re-issues the
|
|
18702
|
-
// same request on the same model.
|
|
18703
|
-
// steering instruction
|
|
18704
|
-
//
|
|
18705
|
-
// its spelling; the instruction never lands in durable history.
|
|
18738
|
+
// same request on the same model. Every bounded re-issue carries the
|
|
18739
|
+
// transient steering instruction, so a deterministic escaper has a reason
|
|
18740
|
+
// to change its spelling; the instruction never lands in durable history.
|
|
18706
18741
|
//
|
|
18707
18742
|
// The retries are un-charged by design, so a deterministic escaper
|
|
18708
18743
|
// would otherwise loop forever: each continuation is a fresh loop with
|
|
18709
|
-
// a fresh in-loop resample budget
|
|
18710
|
-
// charge
|
|
18711
|
-
//
|
|
18712
|
-
// fail-closed answer
|
|
18713
|
-
// rejection. New user turns reset the budget in #resetDefaultFallbackForNewTurn.
|
|
18744
|
+
// a fresh in-loop resample budget. After that bounded recovery budget is
|
|
18745
|
+
// spent, charge the active model once and advance to the next eligible
|
|
18746
|
+
// fallback entry. Only the final eligible model returns the terminal
|
|
18747
|
+
// fail-closed answer.
|
|
18714
18748
|
this.#escapedNonAsciiManagedRetries += 1;
|
|
18715
18749
|
if (this.#escapedNonAsciiManagedRetries > MAX_ESCAPED_NONASCII_MANAGED_RETRIES) {
|
|
18750
|
+
const errorMessage =
|
|
18751
|
+
`Managed fallback retried the escaped non-ASCII tool-call turn ${MAX_ESCAPED_NONASCII_MANAGED_RETRIES} times without a literal-UTF-8 re-issue; ` +
|
|
18752
|
+
`giving up on this model so the next eligible fallback can re-issue with literal UTF-8.`;
|
|
18753
|
+
const controller = this.#defaultFallbackChain();
|
|
18754
|
+
const fallbackFrom = controller.currentSelector();
|
|
18755
|
+
const previousModel = this.model;
|
|
18756
|
+
const previousThinkingLevel = this.thinkingLevel;
|
|
18757
|
+
if (this.model) {
|
|
18758
|
+
this.#escapedNonAsciiExhaustedModelKeys.add(`${this.model.provider}\u0000${this.model.id}`);
|
|
18759
|
+
}
|
|
18760
|
+
controller.discardStartedAttempt();
|
|
18761
|
+
const controllerStateBeforeFailure = controller.snapshotRuntimeState();
|
|
18762
|
+
const advanced = controller.recordEscapedArgumentsFailure(errorMessage, false);
|
|
18763
|
+
this.#escapedNonAsciiManagedRetries = 0;
|
|
18764
|
+
const abortEpoch = this.#abortAdmissionEpoch;
|
|
18765
|
+
if (advanced) {
|
|
18766
|
+
const transitionGeneration = ++this.#fallbackTransitionGeneration;
|
|
18767
|
+
return {
|
|
18768
|
+
type: "retry",
|
|
18769
|
+
continuation: async ownership => {
|
|
18770
|
+
const restoreOwnedTransition = async () => {
|
|
18771
|
+
if (this.#fallbackTransitionGeneration !== transitionGeneration) return;
|
|
18772
|
+
this.#fallbackTransitionGeneration++;
|
|
18773
|
+
if (!this.agent.state.isStreaming)
|
|
18774
|
+
await this.#restoreDefaultFallbackTransition(
|
|
18775
|
+
controller,
|
|
18776
|
+
controllerStateBeforeFailure,
|
|
18777
|
+
previousModel,
|
|
18778
|
+
previousThinkingLevel,
|
|
18779
|
+
);
|
|
18780
|
+
};
|
|
18781
|
+
const continuationCancelled =
|
|
18782
|
+
!ownership.isCurrent() ||
|
|
18783
|
+
ownership.lease.signal.aborted ||
|
|
18784
|
+
cancellationSignal?.aborted ||
|
|
18785
|
+
this.#abortAdmissionEpoch !== abortEpoch ||
|
|
18786
|
+
this.#fallbackTransitionGeneration !== transitionGeneration;
|
|
18787
|
+
if (continuationCancelled) {
|
|
18788
|
+
await restoreOwnedTransition();
|
|
18789
|
+
return;
|
|
18790
|
+
}
|
|
18791
|
+
if (!controller.advance()) {
|
|
18792
|
+
this.#fallbackTransitionGeneration++;
|
|
18793
|
+
this.#defaultFallbackExhaustedLastTurn = true;
|
|
18794
|
+
this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
|
|
18795
|
+
stopReason: "exhausted",
|
|
18796
|
+
messages: [
|
|
18797
|
+
this.#managedFallbackExhaustionMessage(
|
|
18798
|
+
outcome.message,
|
|
18799
|
+
`Managed fallback exhausted every eligible model after escaped non-ASCII tool-call retries; the run fails closed. ${errorMessage}`,
|
|
18800
|
+
),
|
|
18801
|
+
],
|
|
18802
|
+
});
|
|
18803
|
+
return;
|
|
18804
|
+
}
|
|
18805
|
+
const switched = await this.#advanceDefaultFallback(
|
|
18806
|
+
controller,
|
|
18807
|
+
"escaped_non_ascii",
|
|
18808
|
+
1,
|
|
18809
|
+
cancellationSignal,
|
|
18810
|
+
abortEpoch,
|
|
18811
|
+
controllerStateBeforeFailure,
|
|
18812
|
+
previousModel,
|
|
18813
|
+
previousThinkingLevel,
|
|
18814
|
+
{ emitSwitchEvent: false },
|
|
18815
|
+
transitionGeneration,
|
|
18816
|
+
);
|
|
18817
|
+
if (!switched) {
|
|
18818
|
+
if (
|
|
18819
|
+
this.#abortAdmissionEpoch !== abortEpoch ||
|
|
18820
|
+
cancellationSignal?.aborted ||
|
|
18821
|
+
this.#fallbackTransitionGeneration !== transitionGeneration
|
|
18822
|
+
) {
|
|
18823
|
+
await restoreOwnedTransition();
|
|
18824
|
+
return;
|
|
18825
|
+
}
|
|
18826
|
+
this.#fallbackTransitionGeneration++;
|
|
18827
|
+
this.#defaultFallbackExhaustedLastTurn = true;
|
|
18828
|
+
this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
|
|
18829
|
+
stopReason: "exhausted",
|
|
18830
|
+
messages: [
|
|
18831
|
+
this.#managedFallbackExhaustionMessage(
|
|
18832
|
+
outcome.message,
|
|
18833
|
+
`Managed fallback exhausted every eligible model after escaped non-ASCII tool-call retries; the run fails closed. ${errorMessage}`,
|
|
18834
|
+
),
|
|
18835
|
+
],
|
|
18836
|
+
});
|
|
18837
|
+
return;
|
|
18838
|
+
}
|
|
18839
|
+
const continuation = this.agent.continue({
|
|
18840
|
+
...this.#managedFallbackPromptOptions(),
|
|
18841
|
+
transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
|
|
18842
|
+
});
|
|
18843
|
+
if (
|
|
18844
|
+
cancellationSignal?.aborted ||
|
|
18845
|
+
this.#abortAdmissionEpoch !== abortEpoch ||
|
|
18846
|
+
this.#fallbackTransitionGeneration !== transitionGeneration
|
|
18847
|
+
) {
|
|
18848
|
+
await restoreOwnedTransition();
|
|
18849
|
+
return;
|
|
18850
|
+
}
|
|
18851
|
+
this.#emitFallbackSwitchEvent(
|
|
18852
|
+
controller,
|
|
18853
|
+
fallbackFrom,
|
|
18854
|
+
controller.currentSelector(),
|
|
18855
|
+
"escaped_non_ascii",
|
|
18856
|
+
1,
|
|
18857
|
+
);
|
|
18858
|
+
await continuation;
|
|
18859
|
+
this.#fallbackTransitionGeneration++;
|
|
18860
|
+
},
|
|
18861
|
+
};
|
|
18862
|
+
}
|
|
18863
|
+
controller.advance();
|
|
18864
|
+
if (this.#abortAdmissionEpoch !== abortEpoch || cancellationSignal?.aborted)
|
|
18865
|
+
return { type: "terminal", terminal: { stopReason: "cancelled" } };
|
|
18866
|
+
this.#defaultFallbackExhaustedLastTurn = true;
|
|
18716
18867
|
return this.#managedFallbackExhaustionDecision(
|
|
18717
18868
|
outcome.message,
|
|
18718
|
-
`Managed fallback
|
|
18869
|
+
`Managed fallback exhausted every eligible model after escaped non-ASCII tool-call retries; the run fails closed. ${errorMessage}`,
|
|
18719
18870
|
);
|
|
18720
18871
|
}
|
|
18721
18872
|
this.#defaultFallbackChain().discardStartedAttempt();
|
|
18722
|
-
const steering = outcome.steeringPending === true;
|
|
18723
18873
|
return {
|
|
18724
18874
|
type: "retry",
|
|
18725
18875
|
continuation: async ownership => {
|
|
18726
18876
|
if (!ownership.isCurrent() || ownership.lease.signal.aborted) return;
|
|
18727
18877
|
await this.agent.continue({
|
|
18728
18878
|
...this.#managedFallbackPromptOptions(),
|
|
18729
|
-
|
|
18879
|
+
transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
|
|
18730
18880
|
});
|
|
18731
18881
|
},
|
|
18732
18882
|
};
|
|
@@ -18798,6 +18948,7 @@ export class AgentSession {
|
|
|
18798
18948
|
return {
|
|
18799
18949
|
...discarded,
|
|
18800
18950
|
content: [{ type: "text", text: "" }],
|
|
18951
|
+
providerPayload: undefined,
|
|
18801
18952
|
stopReason: "error",
|
|
18802
18953
|
errorMessage,
|
|
18803
18954
|
timestamp: Date.now(),
|
|
@@ -18842,45 +18993,162 @@ export class AgentSession {
|
|
|
18842
18993
|
return undefined;
|
|
18843
18994
|
}
|
|
18844
18995
|
|
|
18996
|
+
async #restoreDefaultFallbackTransition(
|
|
18997
|
+
controller: FallbackChainController,
|
|
18998
|
+
rollbackState: FallbackChainRuntimeState | undefined,
|
|
18999
|
+
previousModel: Model | undefined,
|
|
19000
|
+
previousThinkingLevel: ThinkingLevel | undefined,
|
|
19001
|
+
): Promise<void> {
|
|
19002
|
+
if (rollbackState) controller.restoreRuntimeState(rollbackState);
|
|
19003
|
+
if (previousModel && this.model !== previousModel) {
|
|
19004
|
+
const previousEditMode = this.#resolveActiveEditMode();
|
|
19005
|
+
this.#setModelAuthoritatively(previousModel, "fallback-switch");
|
|
19006
|
+
this.#thinkingLevel = previousThinkingLevel;
|
|
19007
|
+
this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
|
|
19008
|
+
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
19009
|
+
}
|
|
19010
|
+
if (previousModel && this.model === previousModel && this.thinkingLevel !== previousThinkingLevel) {
|
|
19011
|
+
this.#thinkingLevel = previousThinkingLevel;
|
|
19012
|
+
this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
|
|
19013
|
+
}
|
|
19014
|
+
}
|
|
19015
|
+
|
|
18845
19016
|
async #advanceDefaultFallback(
|
|
18846
19017
|
controller: FallbackChainController,
|
|
18847
19018
|
reason: string,
|
|
18848
19019
|
attemptsUsed: number,
|
|
19020
|
+
signal?: AbortSignal,
|
|
19021
|
+
abortEpoch?: number,
|
|
19022
|
+
rollbackState?: FallbackChainRuntimeState,
|
|
19023
|
+
previousModel?: Model,
|
|
19024
|
+
previousThinkingLevel?: ThinkingLevel,
|
|
19025
|
+
options?: { emitSwitchEvent?: boolean },
|
|
19026
|
+
transitionGeneration?: number,
|
|
18849
19027
|
): Promise<boolean> {
|
|
19028
|
+
const promptAbortSignal = this.#promptPreflightAbortController.signal;
|
|
19029
|
+
const cancellationSignal = signal ? AbortSignal.any([signal, promptAbortSignal]) : promptAbortSignal;
|
|
19030
|
+
const transitionStillOwned = () =>
|
|
19031
|
+
transitionGeneration === undefined || this.#fallbackTransitionGeneration === transitionGeneration;
|
|
19032
|
+
const restoreFallbackTransition = () =>
|
|
19033
|
+
transitionStillOwned()
|
|
19034
|
+
? this.#restoreDefaultFallbackTransition(controller, rollbackState, previousModel, previousThinkingLevel)
|
|
19035
|
+
: Promise.resolve();
|
|
19036
|
+
const awaitWithCancellation = async <T>(pending: Promise<T>) => {
|
|
19037
|
+
const cancellation = Promise.withResolvers<"aborted">();
|
|
19038
|
+
const onAbort = () => cancellation.resolve("aborted");
|
|
19039
|
+
cancellationSignal.addEventListener("abort", onAbort, { once: true });
|
|
19040
|
+
try {
|
|
19041
|
+
return await Promise.race([
|
|
19042
|
+
pending.then(value => ({ kind: "resolved" as const, value })),
|
|
19043
|
+
cancellation.promise.then(() => ({ kind: "aborted" as const })),
|
|
19044
|
+
]);
|
|
19045
|
+
} finally {
|
|
19046
|
+
cancellationSignal.removeEventListener("abort", onAbort);
|
|
19047
|
+
}
|
|
19048
|
+
};
|
|
19049
|
+
const rollbackCancelled = async (): Promise<boolean> => {
|
|
19050
|
+
await restoreFallbackTransition();
|
|
19051
|
+
return false;
|
|
19052
|
+
};
|
|
19053
|
+
if (
|
|
19054
|
+
cancellationSignal.aborted ||
|
|
19055
|
+
(abortEpoch !== undefined && this.#abortAdmissionEpoch !== abortEpoch) ||
|
|
19056
|
+
!transitionStillOwned()
|
|
19057
|
+
)
|
|
19058
|
+
return await rollbackCancelled();
|
|
18850
19059
|
while (!controller.isExhausted()) {
|
|
18851
19060
|
const selector = controller.currentSelector();
|
|
18852
19061
|
if (!selector) return false;
|
|
18853
19062
|
const profileAliasIntent = this.#persistedModelProfileAliasIntent("default");
|
|
18854
|
-
const
|
|
18855
|
-
? await
|
|
18856
|
-
|
|
18857
|
-
this.#modelRegistry
|
|
18858
|
-
|
|
18859
|
-
|
|
18860
|
-
|
|
18861
|
-
|
|
18862
|
-
|
|
18863
|
-
|
|
19063
|
+
const profileResolution = profileAliasIntent
|
|
19064
|
+
? await (async () => {
|
|
19065
|
+
const canonicalSessionId = this.agent.providerSessionId ?? this.sessionId;
|
|
19066
|
+
const previousCanonicalVariant = this.#modelRegistry.getSessionCanonicalVariant(canonicalSessionId);
|
|
19067
|
+
const restoreCanonicalVariant = () => {
|
|
19068
|
+
if (previousCanonicalVariant !== undefined) {
|
|
19069
|
+
this.#modelRegistry.restoreSessionCanonicalVariant(
|
|
19070
|
+
canonicalSessionId,
|
|
19071
|
+
previousCanonicalVariant,
|
|
19072
|
+
);
|
|
19073
|
+
} else {
|
|
19074
|
+
this.#modelRegistry.clearCanonicalVariant(canonicalSessionId);
|
|
19075
|
+
}
|
|
19076
|
+
};
|
|
19077
|
+
try {
|
|
19078
|
+
const pendingResolution = resolveModelChainWithAuth(
|
|
19079
|
+
[selector],
|
|
19080
|
+
this.#modelRegistry,
|
|
19081
|
+
this.settings,
|
|
19082
|
+
this.credentialSessionId,
|
|
19083
|
+
{
|
|
19084
|
+
managedFallback: true,
|
|
19085
|
+
...profileAliasIntent,
|
|
19086
|
+
canonicalSessionId,
|
|
19087
|
+
credentialSessionId: this.credentialSessionId,
|
|
19088
|
+
signal: cancellationSignal,
|
|
19089
|
+
},
|
|
19090
|
+
);
|
|
19091
|
+
return await awaitWithCancellation(
|
|
19092
|
+
pendingResolution.finally(() => {
|
|
19093
|
+
if (cancellationSignal.aborted) restoreCanonicalVariant();
|
|
19094
|
+
}),
|
|
19095
|
+
);
|
|
19096
|
+
} catch (error) {
|
|
19097
|
+
if (cancellationSignal.aborted) restoreCanonicalVariant();
|
|
19098
|
+
await restoreFallbackTransition();
|
|
19099
|
+
throw error;
|
|
19100
|
+
}
|
|
19101
|
+
})()
|
|
19102
|
+
: undefined;
|
|
19103
|
+
if (profileResolution?.kind === "aborted") return await rollbackCancelled();
|
|
19104
|
+
const resolved =
|
|
19105
|
+
profileResolution?.kind === "resolved"
|
|
19106
|
+
? profileResolution.value
|
|
19107
|
+
: resolveModelRoleValue(selector, this.#modelRegistry.getAvailable(), {
|
|
19108
|
+
settings: this.settings,
|
|
19109
|
+
matchPreferences: { usageOrder: this.settings.getStorage()?.getModelUsageOrder() },
|
|
19110
|
+
modelRegistry: this.#modelRegistry,
|
|
19111
|
+
sessionId: this.agent.providerSessionId ?? this.sessionId,
|
|
18864
19112
|
credentialSessionId: this.credentialSessionId,
|
|
18865
|
-
}
|
|
18866
|
-
|
|
18867
|
-
|
|
18868
|
-
|
|
18869
|
-
|
|
18870
|
-
|
|
18871
|
-
|
|
18872
|
-
credentialSessionId: this.credentialSessionId,
|
|
18873
|
-
});
|
|
19113
|
+
});
|
|
19114
|
+
if (
|
|
19115
|
+
cancellationSignal.aborted ||
|
|
19116
|
+
(abortEpoch !== undefined && this.#abortAdmissionEpoch !== abortEpoch) ||
|
|
19117
|
+
!transitionStillOwned()
|
|
19118
|
+
)
|
|
19119
|
+
return await rollbackCancelled();
|
|
18874
19120
|
if (!resolved.model) {
|
|
18875
19121
|
controller.onResolutionSkip("unknown_model");
|
|
18876
19122
|
continue;
|
|
18877
19123
|
}
|
|
19124
|
+
const resolvedModel = resolved.model;
|
|
19125
|
+
if (this.#escapedNonAsciiExhaustedModelKeys.has(`${resolved.model.provider}\u0000${resolved.model.id}`)) {
|
|
19126
|
+
controller.onResolutionSkip("escaped_non_ascii_model_exhausted");
|
|
19127
|
+
continue;
|
|
19128
|
+
}
|
|
18878
19129
|
const managedCursorUnavailable = managedCursorFallbackUnavailableReason(resolved.model, selector);
|
|
18879
19130
|
if (managedCursorUnavailable) {
|
|
18880
19131
|
controller.onResolutionSkip(managedCursorUnavailable);
|
|
18881
19132
|
continue;
|
|
18882
19133
|
}
|
|
18883
|
-
const
|
|
19134
|
+
const keyResult = await (async () => {
|
|
19135
|
+
try {
|
|
19136
|
+
return await awaitWithCancellation(
|
|
19137
|
+
this.#modelRegistry.getApiKey(resolvedModel, this.credentialSessionId),
|
|
19138
|
+
);
|
|
19139
|
+
} catch (error) {
|
|
19140
|
+
await rollbackCancelled();
|
|
19141
|
+
throw error;
|
|
19142
|
+
}
|
|
19143
|
+
})();
|
|
19144
|
+
if (keyResult.kind === "aborted") return await rollbackCancelled();
|
|
19145
|
+
const key = keyResult.value;
|
|
19146
|
+
if (
|
|
19147
|
+
cancellationSignal.aborted ||
|
|
19148
|
+
(abortEpoch !== undefined && this.#abortAdmissionEpoch !== abortEpoch) ||
|
|
19149
|
+
!transitionStillOwned()
|
|
19150
|
+
)
|
|
19151
|
+
return await rollbackCancelled();
|
|
18884
19152
|
if (!isAuthenticated(key) && key !== kNoAuth) {
|
|
18885
19153
|
controller.onResolutionSkip("unauthenticated");
|
|
18886
19154
|
continue;
|
|
@@ -18889,33 +19157,64 @@ export class AgentSession {
|
|
|
18889
19157
|
controller.tried.at(-1)?.selector ?? controller.chain.entries[controller.activeIndex - 1] ?? selector;
|
|
18890
19158
|
const to = selector;
|
|
18891
19159
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
19160
|
+
this.#escapedNonAsciiManagedRetries = 0;
|
|
18892
19161
|
this.#setModelAuthoritatively(resolved.model, "fallback-switch");
|
|
18893
|
-
this
|
|
19162
|
+
this.#thinkingLevel = resolveThinkingLevelForModel(
|
|
19163
|
+
this.model,
|
|
18894
19164
|
this.#thinkingLevelForResolvedFallback(resolved.explicitThinkingLevel, resolved.thinkingLevel),
|
|
18895
19165
|
);
|
|
19166
|
+
this.agent.setThinkingLevel(toReasoningEffort(this.#thinkingLevel));
|
|
18896
19167
|
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
18897
|
-
if (
|
|
18898
|
-
|
|
18899
|
-
|
|
18900
|
-
|
|
18901
|
-
|
|
18902
|
-
|
|
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
|
-
});
|
|
19168
|
+
if (
|
|
19169
|
+
cancellationSignal.aborted ||
|
|
19170
|
+
(abortEpoch !== undefined && this.#abortAdmissionEpoch !== abortEpoch) ||
|
|
19171
|
+
!transitionStillOwned()
|
|
19172
|
+
) {
|
|
19173
|
+
return await rollbackCancelled();
|
|
18913
19174
|
}
|
|
19175
|
+
if (options?.emitSwitchEvent ?? true)
|
|
19176
|
+
this.#emitFallbackSwitchEvent(controller, from, to, reason, attemptsUsed);
|
|
19177
|
+
if (
|
|
19178
|
+
cancellationSignal.aborted ||
|
|
19179
|
+
(abortEpoch !== undefined && this.#abortAdmissionEpoch !== abortEpoch) ||
|
|
19180
|
+
!transitionStillOwned()
|
|
19181
|
+
)
|
|
19182
|
+
return await rollbackCancelled();
|
|
18914
19183
|
return true;
|
|
18915
19184
|
}
|
|
18916
19185
|
return false;
|
|
18917
19186
|
}
|
|
18918
19187
|
|
|
19188
|
+
#emitFallbackSwitchEvent(
|
|
19189
|
+
controller: FallbackChainController,
|
|
19190
|
+
from: string | undefined,
|
|
19191
|
+
to: string | undefined,
|
|
19192
|
+
reason: string,
|
|
19193
|
+
attemptsUsed: number,
|
|
19194
|
+
): void {
|
|
19195
|
+
if (!from || !to || from === to) return;
|
|
19196
|
+
const event: Extract<AgentSessionEvent, { type: "model_fallback_switched" }> = {
|
|
19197
|
+
type: "model_fallback_switched",
|
|
19198
|
+
eventId: crypto.randomUUID(),
|
|
19199
|
+
from,
|
|
19200
|
+
to,
|
|
19201
|
+
reason,
|
|
19202
|
+
role:
|
|
19203
|
+
controller.chain.origin === "subagent"
|
|
19204
|
+
? (controller.chain.identity ?? controller.chain.role)
|
|
19205
|
+
: controller.chain.role,
|
|
19206
|
+
scope: controller.chain.origin === "subagent" ? "subagent-call" : "session",
|
|
19207
|
+
activeIndex: controller.activeIndex,
|
|
19208
|
+
chainLength: controller.chain.entries.length,
|
|
19209
|
+
attemptsUsed,
|
|
19210
|
+
};
|
|
19211
|
+
if (this.#eventListeners.length === 0) {
|
|
19212
|
+
this.#pendingFallbackSwitches.push(event);
|
|
19213
|
+
} else {
|
|
19214
|
+
this.#emit(event);
|
|
19215
|
+
}
|
|
19216
|
+
}
|
|
19217
|
+
|
|
18919
19218
|
#emitResolutionFallbackSwitch(controller: FallbackChainController): void {
|
|
18920
19219
|
if (controller.activeIndex <= 0) return;
|
|
18921
19220
|
const to = controller.currentSelector();
|
|
@@ -19292,6 +19591,12 @@ export class AgentSession {
|
|
|
19292
19591
|
}
|
|
19293
19592
|
|
|
19294
19593
|
const retry = async (ownership?: ManagedAttemptContinuationOwnership): Promise<void> => {
|
|
19594
|
+
const activePromptHandle = this.activePromptHandle;
|
|
19595
|
+
const cancellationSignal =
|
|
19596
|
+
ownership?.domain.signal ??
|
|
19597
|
+
(activePromptHandle ? this.#runCancellationDomains.lookup(activePromptHandle)?.signal : undefined);
|
|
19598
|
+
if (ownership && (!ownership.isCurrent() || cancellationSignal?.aborted)) return;
|
|
19599
|
+
const abortEpoch = this.#abortAdmissionEpoch;
|
|
19295
19600
|
let quotaPoolExhausted = false;
|
|
19296
19601
|
if (managedFallback && !credentialRotated && !providerRetryCeilingReached) {
|
|
19297
19602
|
const mark = await this.#markFailedCredential(trigger);
|
|
@@ -19300,13 +19605,55 @@ export class AgentSession {
|
|
|
19300
19605
|
}
|
|
19301
19606
|
let advanced = outcome !== "advance";
|
|
19302
19607
|
let resolutionError: unknown;
|
|
19608
|
+
let controllerStateBeforeAdvance: FallbackChainRuntimeState | undefined;
|
|
19609
|
+
let previousModel: Model | undefined;
|
|
19610
|
+
let previousThinkingLevel: ThinkingLevel | undefined;
|
|
19303
19611
|
if (outcome === "advance") {
|
|
19612
|
+
controllerStateBeforeAdvance = controller.snapshotRuntimeState();
|
|
19613
|
+
const failedEntryIndex = controller.activeIndex - 1;
|
|
19614
|
+
if (failedEntryIndex >= 0 && failedEntryIndex < controller.chain.entries.length) {
|
|
19615
|
+
controllerStateBeforeAdvance.activeIndex = failedEntryIndex;
|
|
19616
|
+
controllerStateBeforeAdvance.attemptsUsed = 0;
|
|
19617
|
+
controllerStateBeforeAdvance.totalAttemptsUsed = Math.max(
|
|
19618
|
+
0,
|
|
19619
|
+
controllerStateBeforeAdvance.totalAttemptsUsed - 1,
|
|
19620
|
+
);
|
|
19621
|
+
controllerStateBeforeAdvance.attemptStarted = false;
|
|
19622
|
+
controllerStateBeforeAdvance.exhaustedForTurn = false;
|
|
19623
|
+
controllerStateBeforeAdvance.tried = controllerStateBeforeAdvance.tried.slice(0, -1);
|
|
19624
|
+
}
|
|
19625
|
+
previousModel = this.model;
|
|
19626
|
+
previousThinkingLevel = this.thinkingLevel;
|
|
19304
19627
|
try {
|
|
19305
|
-
advanced = await this.#advanceDefaultFallback(
|
|
19628
|
+
advanced = await this.#advanceDefaultFallback(
|
|
19629
|
+
controller,
|
|
19630
|
+
trigger.class,
|
|
19631
|
+
attemptsUsed,
|
|
19632
|
+
cancellationSignal,
|
|
19633
|
+
abortEpoch,
|
|
19634
|
+
controllerStateBeforeAdvance,
|
|
19635
|
+
previousModel,
|
|
19636
|
+
previousThinkingLevel,
|
|
19637
|
+
);
|
|
19306
19638
|
} catch (error) {
|
|
19307
19639
|
resolutionError = error;
|
|
19308
19640
|
}
|
|
19309
19641
|
}
|
|
19642
|
+
if (ownership && (!ownership.isCurrent() || cancellationSignal?.aborted)) {
|
|
19643
|
+
if (this.#abortAdmissionEpoch === abortEpoch) {
|
|
19644
|
+
await this.#restoreDefaultFallbackTransition(
|
|
19645
|
+
controller,
|
|
19646
|
+
controllerStateBeforeAdvance,
|
|
19647
|
+
previousModel,
|
|
19648
|
+
previousThinkingLevel,
|
|
19649
|
+
);
|
|
19650
|
+
}
|
|
19651
|
+
return;
|
|
19652
|
+
}
|
|
19653
|
+
if (this.#abortAdmissionEpoch !== abortEpoch) {
|
|
19654
|
+
controller.resetForNewTurn();
|
|
19655
|
+
return;
|
|
19656
|
+
}
|
|
19310
19657
|
if (!advanced) {
|
|
19311
19658
|
let errorMessage = resolutionError
|
|
19312
19659
|
? `${this.#fallbackExhaustionError(controller)}; resolution failed: ${resolutionError instanceof Error ? resolutionError.message : String(resolutionError)}`
|
|
@@ -155,6 +155,23 @@ export class FallbackChainController {
|
|
|
155
155
|
return this.advance() ? "advance" : "exhausted";
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Record one bounded escaped-argument exhaustion for the active model and
|
|
160
|
+
* advance immediately. The discarded wire attempts are intentionally not
|
|
161
|
+
* provider charges; this single policy charge prevents the same model from
|
|
162
|
+
* being retried forever while still allowing every eligible fallback model
|
|
163
|
+
* its own literal-UTF-8 recovery budget.
|
|
164
|
+
*/
|
|
165
|
+
recordEscapedArgumentsFailure(reason: string, advance = true): boolean {
|
|
166
|
+
const selector = this.currentSelector();
|
|
167
|
+
if (!selector || this.exhaustedForTurn) return false;
|
|
168
|
+
this.#attemptStarted = false;
|
|
169
|
+
this.attemptsUsed = this.maxAttempts;
|
|
170
|
+
this.#totalAttemptsUsed += 1;
|
|
171
|
+
this.tried.push({ selector, triggerClass: "unknown", reason });
|
|
172
|
+
return advance ? this.advance() : this.activeIndex + 1 < this.chain.entries.length;
|
|
173
|
+
}
|
|
174
|
+
|
|
158
175
|
advance(): boolean {
|
|
159
176
|
if (this.exhaustedForTurn) return false;
|
|
160
177
|
this.activeIndex += 1;
|