@oh-my-pi/pi-coding-agent 16.4.5 → 16.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/cli.js +3052 -3005
- package/dist/types/cli/bench-cli.d.ts +1 -7
- package/dist/types/cli/usage-cli.d.ts +1 -0
- package/dist/types/commands/usage.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +3 -8
- package/dist/types/modes/components/model-browser.d.ts +3 -0
- package/dist/types/modes/components/model-hub.d.ts +4 -3
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/queue-input.d.ts +8 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/session/agent-storage.d.ts +57 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +0 -1
- package/scripts/compile-binary.ts +4 -3
- package/src/cli/bench-cli.ts +7 -26
- package/src/cli/usage-cli.ts +11 -0
- package/src/commands/usage.ts +13 -2
- package/src/config/settings-schema.ts +1 -1
- package/src/modes/components/advisor-config.ts +3 -1
- package/src/modes/components/custom-editor.test.ts +58 -1
- package/src/modes/components/custom-editor.ts +42 -11
- package/src/modes/components/model-browser.ts +114 -33
- package/src/modes/components/model-hub.ts +455 -108
- package/src/modes/components/usage-row.ts +5 -6
- package/src/modes/controllers/input-controller.ts +140 -6
- package/src/modes/controllers/selector-controller.ts +20 -13
- package/src/modes/controllers/todo-command-controller.ts +1 -2
- package/src/modes/interactive-mode.ts +5 -0
- package/src/modes/queue-input.ts +132 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +19 -20
- package/src/session/agent-session.ts +184 -48
- package/src/session/agent-storage.ts +330 -3
- package/src/session/history-storage.ts +1 -34
- package/src/slash-commands/builtin-registry.ts +9 -0
|
@@ -1060,6 +1060,7 @@ export interface FreshSessionResult {
|
|
|
1060
1060
|
|
|
1061
1061
|
/** Standard thinking levels */
|
|
1062
1062
|
|
|
1063
|
+
/** `retry.fallbackChains` config: chain key (role name or model selector) → ordered fallback selectors. */
|
|
1063
1064
|
type RetryFallbackChains = Record<string, string[]>;
|
|
1064
1065
|
|
|
1065
1066
|
type RetryFallbackRevertPolicy = "never" | "cooldown-expiry";
|
|
@@ -1072,6 +1073,7 @@ interface RetryFallbackSelector {
|
|
|
1072
1073
|
}
|
|
1073
1074
|
|
|
1074
1075
|
interface ActiveRetryFallbackState {
|
|
1076
|
+
/** Chain key that produced this fallback: a model-role name or a model-selector key. */
|
|
1075
1077
|
role: string;
|
|
1076
1078
|
originalSelector: string;
|
|
1077
1079
|
originalThinkingLevel: ConfiguredThinkingLevel | undefined;
|
|
@@ -1099,6 +1101,24 @@ function parseRetryFallbackSelector(
|
|
|
1099
1101
|
};
|
|
1100
1102
|
}
|
|
1101
1103
|
|
|
1104
|
+
/**
|
|
1105
|
+
* `retry.fallbackChains` keys are either model-role names (`smol`, `default`)
|
|
1106
|
+
* or model selectors (`provider/model-id[:thinking]`). Role names never
|
|
1107
|
+
* contain a slash, so its presence marks a model-keyed chain whose primary is
|
|
1108
|
+
* the key itself — the chain follows the model across role reassignments.
|
|
1109
|
+
*/
|
|
1110
|
+
function isRetryFallbackModelKey(key: string): boolean {
|
|
1111
|
+
return key.includes("/");
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* A `provider/*` fallback-chain key: matches any active model of that provider,
|
|
1116
|
+
* so one entry covers every current and future model behind the provider.
|
|
1117
|
+
*/
|
|
1118
|
+
function isRetryFallbackWildcardKey(key: string): boolean {
|
|
1119
|
+
return key.endsWith("/*");
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1102
1122
|
function formatRetryFallbackSelector(model: Model, thinkingLevel: ThinkingLevel | undefined): string {
|
|
1103
1123
|
return formatModelSelectorValue(formatModelStringWithRouting(model), thinkingLevel);
|
|
1104
1124
|
}
|
|
@@ -3820,6 +3840,16 @@ export class AgentSession {
|
|
|
3820
3840
|
if (event.message.role === "assistant") {
|
|
3821
3841
|
this.#lastAssistantMessage = event.message;
|
|
3822
3842
|
const assistantMsg = event.message as AssistantMessage;
|
|
3843
|
+
// Fold this turn's timing into per-model perf aggregates (drives the
|
|
3844
|
+
// /models TPS/TTFT display). Errored turns measure nothing; aborted
|
|
3845
|
+
// turns with reported usage are still valid throughput samples.
|
|
3846
|
+
if (assistantMsg.stopReason !== "error" && assistantMsg.duration !== undefined) {
|
|
3847
|
+
this.settings.getStorage()?.recordModelPerf(`${assistantMsg.provider}/${assistantMsg.model}`, {
|
|
3848
|
+
outputTokens: assistantMsg.usage.output,
|
|
3849
|
+
durationMs: assistantMsg.duration,
|
|
3850
|
+
ttftMs: assistantMsg.ttft,
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3823
3853
|
if (
|
|
3824
3854
|
assistantMsg.disabledFeatures?.includes("priority") &&
|
|
3825
3855
|
this.#serviceTierByFamily.anthropic === "priority"
|
|
@@ -8724,12 +8754,7 @@ export class AgentSession {
|
|
|
8724
8754
|
|
|
8725
8755
|
#syncTodoPhasesFromBranch(): void {
|
|
8726
8756
|
const phases = getLatestTodoPhasesFromEntries(this.sessionManager.getBranch());
|
|
8727
|
-
|
|
8728
|
-
// so they have no bearing on progress tracking for the new turn.
|
|
8729
|
-
for (const phase of phases) {
|
|
8730
|
-
phase.tasks = phase.tasks.filter(t => t.status !== "completed" && t.status !== "abandoned");
|
|
8731
|
-
}
|
|
8732
|
-
this.setTodoPhases(phases.filter(p => p.tasks.length > 0));
|
|
8757
|
+
this.setTodoPhases(phases);
|
|
8733
8758
|
}
|
|
8734
8759
|
|
|
8735
8760
|
#cloneTodoPhases(phases: TodoPhase[]): TodoPhase[] {
|
|
@@ -13480,36 +13505,68 @@ export class AgentSession {
|
|
|
13480
13505
|
const configuredChains = this.settings.get("retry.fallbackChains");
|
|
13481
13506
|
if (configuredChains === undefined) return;
|
|
13482
13507
|
if (!configuredChains || typeof configuredChains !== "object" || Array.isArray(configuredChains)) {
|
|
13483
|
-
const msg = "retry.fallbackChains must be a mapping of role names to selector arrays.";
|
|
13508
|
+
const msg = "retry.fallbackChains must be a mapping of role names or model selectors to selector arrays.";
|
|
13484
13509
|
logger.warn(msg);
|
|
13485
13510
|
this.configWarnings.push(msg);
|
|
13486
13511
|
return;
|
|
13487
13512
|
}
|
|
13488
13513
|
|
|
13489
|
-
for (const
|
|
13514
|
+
for (const key in configuredChains) {
|
|
13515
|
+
const chain = (configuredChains as RetryFallbackChains)[key];
|
|
13516
|
+
const keyKind = isRetryFallbackModelKey(key) ? "model" : "role";
|
|
13517
|
+
if (keyKind === "model") {
|
|
13518
|
+
if (isRetryFallbackWildcardKey(key)) {
|
|
13519
|
+
const provider = key.slice(0, -2);
|
|
13520
|
+
if (!this.#modelRegistry.getAll().some(model => model.provider === provider)) {
|
|
13521
|
+
const msg = `retry.fallbackChains wildcard key references unknown provider: ${key}`;
|
|
13522
|
+
logger.warn(msg);
|
|
13523
|
+
this.configWarnings.push(msg);
|
|
13524
|
+
}
|
|
13525
|
+
} else {
|
|
13526
|
+
const parsedKey = parseRetryFallbackSelector(key, this.#modelRegistry);
|
|
13527
|
+
if (!parsedKey) {
|
|
13528
|
+
const msg = `Invalid model selector key in retry.fallbackChains: ${key}`;
|
|
13529
|
+
logger.warn(msg);
|
|
13530
|
+
this.configWarnings.push(msg);
|
|
13531
|
+
} else if (!this.#modelRegistry.find(parsedKey.provider, parsedKey.id)) {
|
|
13532
|
+
const msg = `retry.fallbackChains key references unknown model: ${key}`;
|
|
13533
|
+
logger.warn(msg);
|
|
13534
|
+
this.configWarnings.push(msg);
|
|
13535
|
+
}
|
|
13536
|
+
}
|
|
13537
|
+
}
|
|
13490
13538
|
if (!Array.isArray(chain)) {
|
|
13491
|
-
const msg = `Fallback chain for
|
|
13539
|
+
const msg = `Fallback chain for ${keyKind} '${key}' must be an array of selector strings.`;
|
|
13492
13540
|
logger.warn(msg);
|
|
13493
13541
|
this.configWarnings.push(msg);
|
|
13494
13542
|
continue;
|
|
13495
13543
|
}
|
|
13496
13544
|
for (const selectorStr of chain) {
|
|
13497
13545
|
if (typeof selectorStr !== "string") {
|
|
13498
|
-
const msg = `Fallback chain for
|
|
13546
|
+
const msg = `Fallback chain for ${keyKind} '${key}' contains a non-string selector.`;
|
|
13499
13547
|
logger.warn(msg);
|
|
13500
13548
|
this.configWarnings.push(msg);
|
|
13501
13549
|
continue;
|
|
13502
13550
|
}
|
|
13551
|
+
if (isRetryFallbackWildcardKey(selectorStr)) {
|
|
13552
|
+
const provider = selectorStr.slice(0, -2);
|
|
13553
|
+
if (!this.#modelRegistry.getAll().some(model => model.provider === provider)) {
|
|
13554
|
+
const msg = `Fallback chain for ${keyKind} '${key}' references unknown provider: ${selectorStr}`;
|
|
13555
|
+
logger.warn(msg);
|
|
13556
|
+
this.configWarnings.push(msg);
|
|
13557
|
+
}
|
|
13558
|
+
continue;
|
|
13559
|
+
}
|
|
13503
13560
|
const parsed = parseRetryFallbackSelector(selectorStr, this.#modelRegistry);
|
|
13504
13561
|
if (!parsed) {
|
|
13505
|
-
const msg = `Invalid fallback selector format in
|
|
13562
|
+
const msg = `Invalid fallback selector format in ${keyKind} '${key}': ${selectorStr}`;
|
|
13506
13563
|
logger.warn(msg);
|
|
13507
13564
|
this.configWarnings.push(msg);
|
|
13508
13565
|
continue;
|
|
13509
13566
|
}
|
|
13510
13567
|
const exists = this.#modelRegistry.find(parsed.provider, parsed.id);
|
|
13511
13568
|
if (!exists) {
|
|
13512
|
-
const msg = `Fallback chain for
|
|
13569
|
+
const msg = `Fallback chain for ${keyKind} '${key}' references unknown model: ${selectorStr}`;
|
|
13513
13570
|
logger.warn(msg);
|
|
13514
13571
|
this.configWarnings.push(msg);
|
|
13515
13572
|
}
|
|
@@ -13522,6 +13579,8 @@ export class AgentSession {
|
|
|
13522
13579
|
}
|
|
13523
13580
|
|
|
13524
13581
|
#getRetryFallbackPrimarySelector(role: string): RetryFallbackSelector | undefined {
|
|
13582
|
+
if (isRetryFallbackWildcardKey(role)) return undefined;
|
|
13583
|
+
if (isRetryFallbackModelKey(role)) return parseRetryFallbackSelector(role, this.#modelRegistry);
|
|
13525
13584
|
const configuredSelector = this.settings.getModelRole(role);
|
|
13526
13585
|
return configuredSelector ? parseRetryFallbackSelector(configuredSelector, this.#modelRegistry) : undefined;
|
|
13527
13586
|
}
|
|
@@ -13543,6 +13602,13 @@ export class AgentSession {
|
|
|
13543
13602
|
this.#modelRegistry.suppressSelector(currentSelector, Date.now() + cooldownMs);
|
|
13544
13603
|
}
|
|
13545
13604
|
|
|
13605
|
+
/**
|
|
13606
|
+
* Map the failing model selector to the chain key that owns it, by
|
|
13607
|
+
* specificity: an exact model-selector key, then a `provider/*` wildcard,
|
|
13608
|
+
* then a model role whose current assignment matches, then `default`.
|
|
13609
|
+
* Model-oriented keys win over roles so a chain follows the model across
|
|
13610
|
+
* role reassignments.
|
|
13611
|
+
*/
|
|
13546
13612
|
#resolveRetryFallbackRole(currentSelector: string): string | undefined {
|
|
13547
13613
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector, this.#modelRegistry);
|
|
13548
13614
|
if (!parsedCurrent) return undefined;
|
|
@@ -13556,18 +13622,33 @@ export class AgentSession {
|
|
|
13556
13622
|
? formatRetryFallbackBaseSelector(parseRetryFallbackSelector(currentPlainSelector) ?? parsedCurrent)
|
|
13557
13623
|
: undefined;
|
|
13558
13624
|
|
|
13559
|
-
|
|
13560
|
-
|
|
13561
|
-
|
|
13625
|
+
const exactModelKeys: string[] = [];
|
|
13626
|
+
const roleKeys: string[] = [];
|
|
13627
|
+
for (const key in chains) {
|
|
13628
|
+
if (!isRetryFallbackModelKey(key)) roleKeys.push(key);
|
|
13629
|
+
else if (!isRetryFallbackWildcardKey(key)) exactModelKeys.push(key);
|
|
13562
13630
|
}
|
|
13563
|
-
|
|
13564
|
-
|
|
13565
|
-
if (
|
|
13566
|
-
|
|
13567
|
-
|
|
13568
|
-
|
|
13569
|
-
|
|
13631
|
+
const matchesCurrent = (primary: RetryFallbackSelector | undefined): boolean => {
|
|
13632
|
+
if (!primary) return false;
|
|
13633
|
+
if (primary.raw === currentSelector || (currentPlainSelector && primary.raw === currentPlainSelector)) {
|
|
13634
|
+
return true;
|
|
13635
|
+
}
|
|
13636
|
+
const base = formatRetryFallbackBaseSelector(primary);
|
|
13637
|
+
return base === currentBaseSelector || (!!currentPlainBaseSelector && base === currentPlainBaseSelector);
|
|
13638
|
+
};
|
|
13639
|
+
|
|
13640
|
+
// 1. Exact model-selector keys — most specific.
|
|
13641
|
+
for (const key of exactModelKeys) {
|
|
13642
|
+
if (matchesCurrent(this.#getRetryFallbackPrimarySelector(key))) return key;
|
|
13643
|
+
}
|
|
13644
|
+
// 2. Provider wildcard (`provider/*`) — any active model of this provider.
|
|
13645
|
+
const wildcardKey = `${parsedCurrent.provider}/*`;
|
|
13646
|
+
if (Array.isArray(chains[wildcardKey])) return wildcardKey;
|
|
13647
|
+
// 3. Role keys — matched by the role's currently-assigned model.
|
|
13648
|
+
for (const key of roleKeys) {
|
|
13649
|
+
if (matchesCurrent(this.#getRetryFallbackPrimarySelector(key))) return key;
|
|
13570
13650
|
}
|
|
13651
|
+
// 4. The default chain, when default has no explicit role primary.
|
|
13571
13652
|
const defaultChain = chains.default;
|
|
13572
13653
|
if (
|
|
13573
13654
|
Array.isArray(defaultChain) &&
|
|
@@ -13579,13 +13660,45 @@ export class AgentSession {
|
|
|
13579
13660
|
return undefined;
|
|
13580
13661
|
}
|
|
13581
13662
|
|
|
13582
|
-
|
|
13583
|
-
|
|
13584
|
-
|
|
13585
|
-
|
|
13586
|
-
|
|
13663
|
+
/**
|
|
13664
|
+
* Parse one configured chain entry. A `provider/*` entry keeps the failing
|
|
13665
|
+
* model's id and swaps the provider (google-antigravity/x → google/x);
|
|
13666
|
+
* ids the target provider lacks are skipped by the candidate loop's
|
|
13667
|
+
* registry lookup.
|
|
13668
|
+
*/
|
|
13669
|
+
#parseRetryFallbackChainEntry(
|
|
13670
|
+
entry: string,
|
|
13671
|
+
current: RetryFallbackSelector | undefined,
|
|
13672
|
+
): RetryFallbackSelector | undefined {
|
|
13673
|
+
if (isRetryFallbackWildcardKey(entry)) {
|
|
13674
|
+
if (!current) return undefined;
|
|
13675
|
+
const provider = entry.slice(0, -2);
|
|
13676
|
+
return { raw: `${provider}/${current.id}`, provider, id: current.id, thinkingLevel: undefined };
|
|
13677
|
+
}
|
|
13678
|
+
return parseRetryFallbackSelector(entry, this.#modelRegistry);
|
|
13679
|
+
}
|
|
13680
|
+
|
|
13681
|
+
#getRetryFallbackEffectiveChain(role: string, currentSelector?: string): RetryFallbackSelector[] {
|
|
13682
|
+
const parsedCurrent = currentSelector
|
|
13683
|
+
? parseRetryFallbackSelector(currentSelector, this.#modelRegistry)
|
|
13684
|
+
: undefined;
|
|
13685
|
+
const seen = new Set<string>();
|
|
13686
|
+
const chain: RetryFallbackSelector[] = [];
|
|
13687
|
+
if (isRetryFallbackWildcardKey(role)) {
|
|
13688
|
+
// A wildcard key has no fixed primary: the active model is the
|
|
13689
|
+
// primary, followed by the configured provider-level fallbacks.
|
|
13690
|
+
if (parsedCurrent) {
|
|
13691
|
+
chain.push(parsedCurrent);
|
|
13692
|
+
seen.add(parsedCurrent.raw);
|
|
13693
|
+
}
|
|
13694
|
+
} else {
|
|
13695
|
+
const primarySelector = this.#getRetryFallbackPrimarySelector(role);
|
|
13696
|
+
if (!primarySelector) return [];
|
|
13697
|
+
chain.push(primarySelector);
|
|
13698
|
+
seen.add(primarySelector.raw);
|
|
13699
|
+
}
|
|
13587
13700
|
for (const selector of this.#getRetryFallbackChains()[role] ?? []) {
|
|
13588
|
-
const parsed =
|
|
13701
|
+
const parsed = this.#parseRetryFallbackChainEntry(selector, parsedCurrent);
|
|
13589
13702
|
if (!parsed || seen.has(parsed.raw)) continue;
|
|
13590
13703
|
seen.add(parsed.raw);
|
|
13591
13704
|
chain.push(parsed);
|
|
@@ -13594,7 +13707,7 @@ export class AgentSession {
|
|
|
13594
13707
|
}
|
|
13595
13708
|
|
|
13596
13709
|
#findRetryFallbackCandidates(role: string, currentSelector: string): RetryFallbackSelector[] {
|
|
13597
|
-
let chain = this.#getRetryFallbackEffectiveChain(role);
|
|
13710
|
+
let chain = this.#getRetryFallbackEffectiveChain(role, currentSelector);
|
|
13598
13711
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector, this.#modelRegistry);
|
|
13599
13712
|
if (chain.length === 0 && role === "default" && parsedCurrent) {
|
|
13600
13713
|
const chains = this.#getRetryFallbackChains();
|
|
@@ -13607,7 +13720,7 @@ export class AgentSession {
|
|
|
13607
13720
|
const seen = new Set<string>([parsedCurrent.raw]);
|
|
13608
13721
|
chain = [parsedCurrent];
|
|
13609
13722
|
for (const selector of defaultChain) {
|
|
13610
|
-
const parsed =
|
|
13723
|
+
const parsed = this.#parseRetryFallbackChainEntry(selector, parsedCurrent);
|
|
13611
13724
|
if (!parsed || seen.has(parsed.raw)) continue;
|
|
13612
13725
|
seen.add(parsed.raw);
|
|
13613
13726
|
chain.push(parsed);
|
|
@@ -13880,20 +13993,13 @@ export class AgentSession {
|
|
|
13880
13993
|
this.#retryResolve = resolve;
|
|
13881
13994
|
}
|
|
13882
13995
|
|
|
13883
|
-
|
|
13884
|
-
|
|
13885
|
-
|
|
13886
|
-
|
|
13887
|
-
|
|
13888
|
-
|
|
13889
|
-
|
|
13890
|
-
finalError: message.errorMessage,
|
|
13891
|
-
});
|
|
13892
|
-
this.#clearPendingRecoveredRetryErrors();
|
|
13893
|
-
this.#retryAttempt = 0;
|
|
13894
|
-
this.#resolveRetry(); // Resolve so waitForRetry() completes
|
|
13895
|
-
return false;
|
|
13896
|
-
}
|
|
13996
|
+
// All attempts on the current model are spent. Don't fail yet: the
|
|
13997
|
+
// fallback chain below gets one last consult. Credential rotation can
|
|
13998
|
+
// consume the entire budget without the fallback branch ever running
|
|
13999
|
+
// (every rotation sets switchedCredential and skips it), so without
|
|
14000
|
+
// this last resort a provider-wide usage cap never fails over to the
|
|
14001
|
+
// configured chain.
|
|
14002
|
+
const retryBudgetExhausted = this.#retryAttempt > retrySettings.maxRetries;
|
|
13897
14003
|
|
|
13898
14004
|
const errorMessage = message.errorMessage || "Unknown error";
|
|
13899
14005
|
const id = this.#classifyRetryMessage(message);
|
|
@@ -13912,7 +14018,12 @@ export class AgentSession {
|
|
|
13912
14018
|
this.#resetCurrentResponsesProviderSession("stale replay error");
|
|
13913
14019
|
}
|
|
13914
14020
|
|
|
13915
|
-
if (
|
|
14021
|
+
if (
|
|
14022
|
+
!retryBudgetExhausted &&
|
|
14023
|
+
this.model &&
|
|
14024
|
+
!staleOpenAIResponsesReplayError &&
|
|
14025
|
+
AIError.is(id, AIError.Flag.UsageLimit)
|
|
14026
|
+
) {
|
|
13916
14027
|
const retryAfterMs = parsedRetryAfterMs ?? calculateRateLimitBackoffMs(parseRateLimitReason(errorMessage));
|
|
13917
14028
|
const outcome = await this.#modelRegistry.authStorage.markUsageLimitReached(
|
|
13918
14029
|
this.model.provider,
|
|
@@ -13958,7 +14069,9 @@ export class AgentSession {
|
|
|
13958
14069
|
const allowModelFallback = options?.allowModelFallback !== false;
|
|
13959
14070
|
const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
|
|
13960
14071
|
if (!staleOpenAIResponsesReplayError && !switchedCredential && currentSelector) {
|
|
13961
|
-
|
|
14072
|
+
// A refusal chain stops at the retry budget: the exhausted-attempt
|
|
14073
|
+
// last resort is for provider failures, not classifier decisions.
|
|
14074
|
+
if (allowModelFallback && retrySettings.modelFallback && !(retryBudgetExhausted && classifierRefusal)) {
|
|
13962
14075
|
if (!classifierRefusal) {
|
|
13963
14076
|
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
13964
14077
|
}
|
|
@@ -13977,6 +14090,26 @@ export class AgentSession {
|
|
|
13977
14090
|
delayMs = parsedRetryAfterMs;
|
|
13978
14091
|
}
|
|
13979
14092
|
}
|
|
14093
|
+
if (retryBudgetExhausted) {
|
|
14094
|
+
if (!switchedModel) {
|
|
14095
|
+
await this.#persistRetryLifecycleErrorMessage(message);
|
|
14096
|
+
// Max retries exceeded and no fallback model to switch to: emit
|
|
14097
|
+
// final failure and reset.
|
|
14098
|
+
await this.#emitSessionEvent({
|
|
14099
|
+
type: "auto_retry_end",
|
|
14100
|
+
success: false,
|
|
14101
|
+
attempt: this.#retryAttempt - 1,
|
|
14102
|
+
finalError: message.errorMessage,
|
|
14103
|
+
});
|
|
14104
|
+
this.#clearPendingRecoveredRetryErrors();
|
|
14105
|
+
this.#retryAttempt = 0;
|
|
14106
|
+
this.#resolveRetry(); // Resolve so waitForRetry() completes
|
|
14107
|
+
return false;
|
|
14108
|
+
}
|
|
14109
|
+
// The fallback model gets a fresh retry budget — leaving the spent
|
|
14110
|
+
// counter in place would exhaust it again on its first error.
|
|
14111
|
+
this.#retryAttempt = 1;
|
|
14112
|
+
}
|
|
13980
14113
|
if (classifierRefusal && !switchedModel) {
|
|
13981
14114
|
this.#retryAttempt = 0;
|
|
13982
14115
|
this.#resolveRetry();
|
|
@@ -14704,10 +14837,13 @@ export class AgentSession {
|
|
|
14704
14837
|
// Side-channel turns must not share OpenAI/Codex append-only
|
|
14705
14838
|
// conversation state with the main agent turn: IRC and /btw can run
|
|
14706
14839
|
// while the main turn is mid-tool-call. Keep the prompt-cache key
|
|
14707
|
-
// stable, but give provider routing a unique request lineage.
|
|
14840
|
+
// stable, but give provider routing a unique request lineage. The
|
|
14841
|
+
// shared provider state map is still required so Codex can allocate
|
|
14842
|
+
// websocket state under that side-channel session id.
|
|
14708
14843
|
sessionId: `${cacheSessionId}:side:${Snowflake.next()}`,
|
|
14709
14844
|
promptCacheKey: cacheSessionId,
|
|
14710
|
-
preferWebsockets:
|
|
14845
|
+
preferWebsockets: this.#preferWebsockets,
|
|
14846
|
+
providerSessionState: this.#providerSessionState,
|
|
14711
14847
|
reasoning: toReasoningEffort(this.thinkingLevel),
|
|
14712
14848
|
disableReasoning: shouldDisableReasoning(this.thinkingLevel),
|
|
14713
14849
|
hideThinkingSummary: this.agent.hideThinkingSummary,
|