@oh-my-pi/pi-coding-agent 16.4.4 → 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 +62 -0
- package/dist/cli.js +3799 -3729
- package/dist/types/async/job-manager.d.ts +8 -0
- 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 +19 -9
- package/dist/types/config/settings.d.ts +3 -2
- package/dist/types/discovery/helpers.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +36 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +1 -0
- package/dist/types/modes/components/ask-dialog.d.ts +27 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -8
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/model-browser.d.ts +100 -0
- package/dist/types/modes/components/model-hub.d.ts +52 -0
- package/dist/types/modes/components/pause-screen.d.ts +43 -0
- package/dist/types/modes/components/session-selector.d.ts +13 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/queue-input.d.ts +8 -0
- package/dist/types/modes/shared.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/agent-storage.d.ts +57 -0
- package/dist/types/session/session-context.d.ts +9 -0
- package/dist/types/task/executor.d.ts +26 -13
- package/dist/types/task/index.d.ts +12 -11
- package/dist/types/task/label.d.ts +4 -0
- package/dist/types/task/repair-args.d.ts +8 -8
- package/dist/types/task/types.d.ts +31 -56
- package/dist/types/tools/ask.d.ts +12 -0
- package/dist/types/tools/conflict-detect.d.ts +17 -1
- package/dist/types/tools/job.d.ts +16 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +0 -1
- package/scripts/compile-binary.ts +4 -3
- package/src/async/job-manager.ts +9 -0
- 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/commit/agentic/tools/analyze-file.ts +2 -3
- package/src/config/settings-schema.ts +18 -7
- package/src/config/settings.ts +13 -4
- package/src/discovery/helpers.ts +3 -4
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/extensions/types.ts +46 -0
- package/src/irc/bus.ts +61 -20
- package/src/modes/components/__tests__/pause-screen.test.ts +143 -0
- package/src/modes/components/advisor-config.ts +32 -22
- package/src/modes/components/ask-dialog.ts +888 -0
- package/src/modes/components/custom-editor.test.ts +58 -1
- package/src/modes/components/custom-editor.ts +42 -11
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/model-browser.ts +769 -0
- package/src/modes/components/model-hub.ts +2002 -0
- package/src/modes/components/pause-screen.ts +208 -0
- package/src/modes/components/session-selector.ts +299 -42
- package/src/modes/components/tool-execution.ts +2 -0
- package/src/modes/components/usage-row.ts +5 -6
- package/src/modes/controllers/event-controller.ts +8 -2
- package/src/modes/controllers/extension-ui-controller.ts +252 -5
- package/src/modes/controllers/input-controller.ts +140 -6
- package/src/modes/controllers/selector-controller.ts +160 -97
- package/src/modes/controllers/tan-command-controller.ts +1 -1
- package/src/modes/controllers/todo-command-controller.ts +1 -2
- package/src/modes/interactive-mode.ts +8 -0
- package/src/modes/queue-input.ts +132 -0
- package/src/modes/shared.ts +1 -1
- package/src/modes/theme/theme.ts +3 -3
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/ui-helpers.ts +50 -24
- package/src/prompts/agents/scout.md +0 -1
- package/src/prompts/agents/task.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +1 -5
- package/src/prompts/system/subagent-yield-reminder.md +10 -0
- package/src/prompts/system/task-label.md +23 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/task-summary.md +3 -0
- package/src/prompts/tools/task.md +17 -18
- package/src/session/agent-session.ts +186 -49
- package/src/session/agent-storage.ts +330 -3
- package/src/session/history-storage.ts +1 -34
- package/src/session/session-context.test.ts +73 -0
- package/src/session/session-context.ts +43 -26
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +159 -46
- package/src/task/index.ts +377 -239
- package/src/task/label.ts +38 -0
- package/src/task/render.ts +74 -22
- package/src/task/repair-args.ts +20 -31
- package/src/task/spawn-policy.test.ts +4 -4
- package/src/task/types.ts +46 -66
- package/src/tools/ask.ts +233 -40
- package/src/tools/conflict-detect.ts +102 -5
- package/src/tools/index.ts +1 -0
- package/src/tools/irc.ts +20 -11
- package/src/tools/job.ts +158 -18
- package/src/tools/write.ts +70 -6
- package/src/vibe/runtime.ts +1 -1
- package/src/web/search/providers/browser-headers.ts +30 -13
- package/dist/types/modes/components/model-selector.d.ts +0 -37
- package/dist/types/tools/bash-command-fixup.d.ts +0 -3
- package/src/modes/components/model-selector.ts +0 -1291
- package/src/tools/bash-command-fixup.ts +0 -4
|
@@ -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"
|
|
@@ -6891,12 +6921,13 @@ export class AgentSession {
|
|
|
6891
6921
|
* `agent.replaceMessages` or a provider.
|
|
6892
6922
|
*/
|
|
6893
6923
|
buildTranscriptSessionContext(
|
|
6894
|
-
options?: Pick<BuildSessionContextOptions, "collapseCompactedHistory">,
|
|
6924
|
+
options?: Pick<BuildSessionContextOptions, "collapseCompactedHistory" | "keepDanglingToolCalls">,
|
|
6895
6925
|
): SessionContext {
|
|
6896
6926
|
return deobfuscateSessionContext(
|
|
6897
6927
|
this.sessionManager.buildSessionContext({
|
|
6898
6928
|
transcript: true,
|
|
6899
6929
|
collapseCompactedHistory: options?.collapseCompactedHistory,
|
|
6930
|
+
keepDanglingToolCalls: options?.keepDanglingToolCalls,
|
|
6900
6931
|
}),
|
|
6901
6932
|
this.#obfuscator,
|
|
6902
6933
|
);
|
|
@@ -8723,12 +8754,7 @@ export class AgentSession {
|
|
|
8723
8754
|
|
|
8724
8755
|
#syncTodoPhasesFromBranch(): void {
|
|
8725
8756
|
const phases = getLatestTodoPhasesFromEntries(this.sessionManager.getBranch());
|
|
8726
|
-
|
|
8727
|
-
// so they have no bearing on progress tracking for the new turn.
|
|
8728
|
-
for (const phase of phases) {
|
|
8729
|
-
phase.tasks = phase.tasks.filter(t => t.status !== "completed" && t.status !== "abandoned");
|
|
8730
|
-
}
|
|
8731
|
-
this.setTodoPhases(phases.filter(p => p.tasks.length > 0));
|
|
8757
|
+
this.setTodoPhases(phases);
|
|
8732
8758
|
}
|
|
8733
8759
|
|
|
8734
8760
|
#cloneTodoPhases(phases: TodoPhase[]): TodoPhase[] {
|
|
@@ -13479,36 +13505,68 @@ export class AgentSession {
|
|
|
13479
13505
|
const configuredChains = this.settings.get("retry.fallbackChains");
|
|
13480
13506
|
if (configuredChains === undefined) return;
|
|
13481
13507
|
if (!configuredChains || typeof configuredChains !== "object" || Array.isArray(configuredChains)) {
|
|
13482
|
-
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.";
|
|
13483
13509
|
logger.warn(msg);
|
|
13484
13510
|
this.configWarnings.push(msg);
|
|
13485
13511
|
return;
|
|
13486
13512
|
}
|
|
13487
13513
|
|
|
13488
|
-
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
|
+
}
|
|
13489
13538
|
if (!Array.isArray(chain)) {
|
|
13490
|
-
const msg = `Fallback chain for
|
|
13539
|
+
const msg = `Fallback chain for ${keyKind} '${key}' must be an array of selector strings.`;
|
|
13491
13540
|
logger.warn(msg);
|
|
13492
13541
|
this.configWarnings.push(msg);
|
|
13493
13542
|
continue;
|
|
13494
13543
|
}
|
|
13495
13544
|
for (const selectorStr of chain) {
|
|
13496
13545
|
if (typeof selectorStr !== "string") {
|
|
13497
|
-
const msg = `Fallback chain for
|
|
13546
|
+
const msg = `Fallback chain for ${keyKind} '${key}' contains a non-string selector.`;
|
|
13498
13547
|
logger.warn(msg);
|
|
13499
13548
|
this.configWarnings.push(msg);
|
|
13500
13549
|
continue;
|
|
13501
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
|
+
}
|
|
13502
13560
|
const parsed = parseRetryFallbackSelector(selectorStr, this.#modelRegistry);
|
|
13503
13561
|
if (!parsed) {
|
|
13504
|
-
const msg = `Invalid fallback selector format in
|
|
13562
|
+
const msg = `Invalid fallback selector format in ${keyKind} '${key}': ${selectorStr}`;
|
|
13505
13563
|
logger.warn(msg);
|
|
13506
13564
|
this.configWarnings.push(msg);
|
|
13507
13565
|
continue;
|
|
13508
13566
|
}
|
|
13509
13567
|
const exists = this.#modelRegistry.find(parsed.provider, parsed.id);
|
|
13510
13568
|
if (!exists) {
|
|
13511
|
-
const msg = `Fallback chain for
|
|
13569
|
+
const msg = `Fallback chain for ${keyKind} '${key}' references unknown model: ${selectorStr}`;
|
|
13512
13570
|
logger.warn(msg);
|
|
13513
13571
|
this.configWarnings.push(msg);
|
|
13514
13572
|
}
|
|
@@ -13521,6 +13579,8 @@ export class AgentSession {
|
|
|
13521
13579
|
}
|
|
13522
13580
|
|
|
13523
13581
|
#getRetryFallbackPrimarySelector(role: string): RetryFallbackSelector | undefined {
|
|
13582
|
+
if (isRetryFallbackWildcardKey(role)) return undefined;
|
|
13583
|
+
if (isRetryFallbackModelKey(role)) return parseRetryFallbackSelector(role, this.#modelRegistry);
|
|
13524
13584
|
const configuredSelector = this.settings.getModelRole(role);
|
|
13525
13585
|
return configuredSelector ? parseRetryFallbackSelector(configuredSelector, this.#modelRegistry) : undefined;
|
|
13526
13586
|
}
|
|
@@ -13542,6 +13602,13 @@ export class AgentSession {
|
|
|
13542
13602
|
this.#modelRegistry.suppressSelector(currentSelector, Date.now() + cooldownMs);
|
|
13543
13603
|
}
|
|
13544
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
|
+
*/
|
|
13545
13612
|
#resolveRetryFallbackRole(currentSelector: string): string | undefined {
|
|
13546
13613
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector, this.#modelRegistry);
|
|
13547
13614
|
if (!parsedCurrent) return undefined;
|
|
@@ -13555,18 +13622,33 @@ export class AgentSession {
|
|
|
13555
13622
|
? formatRetryFallbackBaseSelector(parseRetryFallbackSelector(currentPlainSelector) ?? parsedCurrent)
|
|
13556
13623
|
: undefined;
|
|
13557
13624
|
|
|
13558
|
-
|
|
13559
|
-
|
|
13560
|
-
|
|
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);
|
|
13561
13630
|
}
|
|
13562
|
-
|
|
13563
|
-
|
|
13564
|
-
if (
|
|
13565
|
-
|
|
13566
|
-
|
|
13567
|
-
|
|
13568
|
-
|
|
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;
|
|
13569
13650
|
}
|
|
13651
|
+
// 4. The default chain, when default has no explicit role primary.
|
|
13570
13652
|
const defaultChain = chains.default;
|
|
13571
13653
|
if (
|
|
13572
13654
|
Array.isArray(defaultChain) &&
|
|
@@ -13578,13 +13660,45 @@ export class AgentSession {
|
|
|
13578
13660
|
return undefined;
|
|
13579
13661
|
}
|
|
13580
13662
|
|
|
13581
|
-
|
|
13582
|
-
|
|
13583
|
-
|
|
13584
|
-
|
|
13585
|
-
|
|
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
|
+
}
|
|
13586
13700
|
for (const selector of this.#getRetryFallbackChains()[role] ?? []) {
|
|
13587
|
-
const parsed =
|
|
13701
|
+
const parsed = this.#parseRetryFallbackChainEntry(selector, parsedCurrent);
|
|
13588
13702
|
if (!parsed || seen.has(parsed.raw)) continue;
|
|
13589
13703
|
seen.add(parsed.raw);
|
|
13590
13704
|
chain.push(parsed);
|
|
@@ -13593,7 +13707,7 @@ export class AgentSession {
|
|
|
13593
13707
|
}
|
|
13594
13708
|
|
|
13595
13709
|
#findRetryFallbackCandidates(role: string, currentSelector: string): RetryFallbackSelector[] {
|
|
13596
|
-
let chain = this.#getRetryFallbackEffectiveChain(role);
|
|
13710
|
+
let chain = this.#getRetryFallbackEffectiveChain(role, currentSelector);
|
|
13597
13711
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector, this.#modelRegistry);
|
|
13598
13712
|
if (chain.length === 0 && role === "default" && parsedCurrent) {
|
|
13599
13713
|
const chains = this.#getRetryFallbackChains();
|
|
@@ -13606,7 +13720,7 @@ export class AgentSession {
|
|
|
13606
13720
|
const seen = new Set<string>([parsedCurrent.raw]);
|
|
13607
13721
|
chain = [parsedCurrent];
|
|
13608
13722
|
for (const selector of defaultChain) {
|
|
13609
|
-
const parsed =
|
|
13723
|
+
const parsed = this.#parseRetryFallbackChainEntry(selector, parsedCurrent);
|
|
13610
13724
|
if (!parsed || seen.has(parsed.raw)) continue;
|
|
13611
13725
|
seen.add(parsed.raw);
|
|
13612
13726
|
chain.push(parsed);
|
|
@@ -13879,20 +13993,13 @@ export class AgentSession {
|
|
|
13879
13993
|
this.#retryResolve = resolve;
|
|
13880
13994
|
}
|
|
13881
13995
|
|
|
13882
|
-
|
|
13883
|
-
|
|
13884
|
-
|
|
13885
|
-
|
|
13886
|
-
|
|
13887
|
-
|
|
13888
|
-
|
|
13889
|
-
finalError: message.errorMessage,
|
|
13890
|
-
});
|
|
13891
|
-
this.#clearPendingRecoveredRetryErrors();
|
|
13892
|
-
this.#retryAttempt = 0;
|
|
13893
|
-
this.#resolveRetry(); // Resolve so waitForRetry() completes
|
|
13894
|
-
return false;
|
|
13895
|
-
}
|
|
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;
|
|
13896
14003
|
|
|
13897
14004
|
const errorMessage = message.errorMessage || "Unknown error";
|
|
13898
14005
|
const id = this.#classifyRetryMessage(message);
|
|
@@ -13911,7 +14018,12 @@ export class AgentSession {
|
|
|
13911
14018
|
this.#resetCurrentResponsesProviderSession("stale replay error");
|
|
13912
14019
|
}
|
|
13913
14020
|
|
|
13914
|
-
if (
|
|
14021
|
+
if (
|
|
14022
|
+
!retryBudgetExhausted &&
|
|
14023
|
+
this.model &&
|
|
14024
|
+
!staleOpenAIResponsesReplayError &&
|
|
14025
|
+
AIError.is(id, AIError.Flag.UsageLimit)
|
|
14026
|
+
) {
|
|
13915
14027
|
const retryAfterMs = parsedRetryAfterMs ?? calculateRateLimitBackoffMs(parseRateLimitReason(errorMessage));
|
|
13916
14028
|
const outcome = await this.#modelRegistry.authStorage.markUsageLimitReached(
|
|
13917
14029
|
this.model.provider,
|
|
@@ -13957,7 +14069,9 @@ export class AgentSession {
|
|
|
13957
14069
|
const allowModelFallback = options?.allowModelFallback !== false;
|
|
13958
14070
|
const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
|
|
13959
14071
|
if (!staleOpenAIResponsesReplayError && !switchedCredential && currentSelector) {
|
|
13960
|
-
|
|
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)) {
|
|
13961
14075
|
if (!classifierRefusal) {
|
|
13962
14076
|
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
13963
14077
|
}
|
|
@@ -13976,6 +14090,26 @@ export class AgentSession {
|
|
|
13976
14090
|
delayMs = parsedRetryAfterMs;
|
|
13977
14091
|
}
|
|
13978
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
|
+
}
|
|
13979
14113
|
if (classifierRefusal && !switchedModel) {
|
|
13980
14114
|
this.#retryAttempt = 0;
|
|
13981
14115
|
this.#resolveRetry();
|
|
@@ -14703,10 +14837,13 @@ export class AgentSession {
|
|
|
14703
14837
|
// Side-channel turns must not share OpenAI/Codex append-only
|
|
14704
14838
|
// conversation state with the main agent turn: IRC and /btw can run
|
|
14705
14839
|
// while the main turn is mid-tool-call. Keep the prompt-cache key
|
|
14706
|
-
// 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.
|
|
14707
14843
|
sessionId: `${cacheSessionId}:side:${Snowflake.next()}`,
|
|
14708
14844
|
promptCacheKey: cacheSessionId,
|
|
14709
|
-
preferWebsockets:
|
|
14845
|
+
preferWebsockets: this.#preferWebsockets,
|
|
14846
|
+
providerSessionState: this.#providerSessionState,
|
|
14710
14847
|
reasoning: toReasoningEffort(this.thinkingLevel),
|
|
14711
14848
|
disableReasoning: shouldDisableReasoning(this.thinkingLevel),
|
|
14712
14849
|
hideThinkingSummary: this.agent.hideThinkingSummary,
|