@oh-my-pi/pi-coding-agent 13.16.4 → 13.17.0
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 +51 -0
- package/package.json +7 -7
- package/src/cli/args.ts +7 -0
- package/src/cli/classify-install-target.ts +50 -0
- package/src/cli/plugin-cli.ts +245 -31
- package/src/commands/plugin.ts +3 -0
- package/src/config/model-registry.ts +37 -0
- package/src/config/model-resolver.ts +18 -3
- package/src/config/settings-schema.ts +24 -13
- package/src/cursor.ts +66 -1
- package/src/discovery/claude-plugins.ts +95 -5
- package/src/discovery/helpers.ts +168 -41
- package/src/discovery/plugin-dir-roots.ts +28 -0
- package/src/discovery/substitute-plugin-root.ts +29 -0
- package/src/extensibility/plugins/index.ts +1 -0
- package/src/extensibility/plugins/marketplace/cache.ts +136 -0
- package/src/extensibility/plugins/marketplace/fetcher.ts +354 -0
- package/src/extensibility/plugins/marketplace/index.ts +6 -0
- package/src/extensibility/plugins/marketplace/manager.ts +528 -0
- package/src/extensibility/plugins/marketplace/registry.ts +181 -0
- package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
- package/src/extensibility/plugins/marketplace/types.ts +177 -0
- package/src/extensibility/skills.ts +3 -3
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/local-protocol.ts +2 -19
- package/src/internal-urls/parse.ts +72 -0
- package/src/internal-urls/router.ts +2 -18
- package/src/lsp/config.ts +9 -0
- package/src/main.ts +50 -1
- package/src/modes/components/plugin-selector.ts +86 -0
- package/src/modes/components/settings-defs.ts +9 -4
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/controllers/mcp-command-controller.ts +14 -0
- package/src/modes/controllers/selector-controller.ts +104 -13
- package/src/modes/interactive-mode.ts +9 -0
- package/src/modes/types.ts +1 -0
- package/src/prompts/agents/reviewer.md +3 -4
- package/src/prompts/tools/bash.md +3 -3
- package/src/sdk.ts +0 -7
- package/src/session/agent-session.ts +292 -6
- package/src/slash-commands/builtin-registry.ts +273 -0
- package/src/tools/bash-skill-urls.ts +48 -5
- package/src/tools/bash.ts +2 -0
- package/src/tools/read.ts +15 -9
- package/src/web/search/code-search.ts +2 -179
- package/src/web/search/index.ts +2 -3
- package/src/web/search/types.ts +1 -5
|
@@ -55,7 +55,12 @@ import { abortableSleep, getAgentDbPath, isEnoent, logger } from "@oh-my-pi/pi-u
|
|
|
55
55
|
import type { AsyncJob, AsyncJobManager } from "../async";
|
|
56
56
|
import type { Rule } from "../capability/rule";
|
|
57
57
|
import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
|
|
58
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
extractExplicitThinkingSelector,
|
|
60
|
+
formatModelString,
|
|
61
|
+
parseModelString,
|
|
62
|
+
resolveModelRoleValue,
|
|
63
|
+
} from "../config/model-resolver";
|
|
59
64
|
import { expandPromptTemplate, type PromptTemplate, renderPromptTemplate } from "../config/prompt-templates";
|
|
60
65
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
61
66
|
import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
|
|
@@ -170,6 +175,8 @@ export type AgentSessionEvent =
|
|
|
170
175
|
}
|
|
171
176
|
| { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
|
|
172
177
|
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
|
|
178
|
+
| { type: "retry_fallback_applied"; from: string; to: string; role: string }
|
|
179
|
+
| { type: "retry_fallback_succeeded"; model: string; role: string }
|
|
173
180
|
| { type: "ttsr_triggered"; rules: Rule[] }
|
|
174
181
|
| { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
|
|
175
182
|
| { type: "todo_auto_clear" };
|
|
@@ -315,6 +322,46 @@ interface HandoffOptions {
|
|
|
315
322
|
|
|
316
323
|
const AUTO_HANDOFF_THRESHOLD_FOCUS = renderPromptTemplate(autoHandoffThresholdFocusPrompt);
|
|
317
324
|
|
|
325
|
+
type RetryFallbackChains = Record<string, string[]>;
|
|
326
|
+
|
|
327
|
+
type RetryFallbackRevertPolicy = "never" | "cooldown-expiry";
|
|
328
|
+
|
|
329
|
+
interface RetryFallbackSelector {
|
|
330
|
+
raw: string;
|
|
331
|
+
provider: string;
|
|
332
|
+
id: string;
|
|
333
|
+
thinkingLevel: ThinkingLevel | undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
interface ActiveRetryFallbackState {
|
|
337
|
+
role: string;
|
|
338
|
+
originalSelector: string;
|
|
339
|
+
originalThinkingLevel: ThinkingLevel | undefined;
|
|
340
|
+
lastAppliedFallbackThinkingLevel: ThinkingLevel | undefined;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function parseRetryFallbackSelector(selector: string): RetryFallbackSelector | undefined {
|
|
344
|
+
const trimmed = selector.trim();
|
|
345
|
+
if (!trimmed) return undefined;
|
|
346
|
+
const parsed = parseModelString(trimmed);
|
|
347
|
+
if (!parsed) return undefined;
|
|
348
|
+
return {
|
|
349
|
+
raw: trimmed,
|
|
350
|
+
provider: parsed.provider,
|
|
351
|
+
id: parsed.id,
|
|
352
|
+
thinkingLevel: parsed.thinkingLevel,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function formatRetryFallbackSelector(model: Model, thinkingLevel: ThinkingLevel | undefined): string {
|
|
357
|
+
const selector = formatModelString(model);
|
|
358
|
+
return thinkingLevel ? `${selector}:${thinkingLevel}` : selector;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): string {
|
|
362
|
+
return `${selector.provider}/${selector.id}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
318
365
|
const noOpUIContext: ExtensionUIContext = {
|
|
319
366
|
select: async (_title, _options, _dialogOptions) => undefined,
|
|
320
367
|
confirm: async (_title, _message, _dialogOptions) => false,
|
|
@@ -352,6 +399,7 @@ export class AgentSession {
|
|
|
352
399
|
readonly sessionManager: SessionManager;
|
|
353
400
|
readonly settings: Settings;
|
|
354
401
|
readonly searchDb: SearchDb | undefined;
|
|
402
|
+
readonly configWarnings: string[] = [];
|
|
355
403
|
|
|
356
404
|
#asyncJobManager: AsyncJobManager | undefined = undefined;
|
|
357
405
|
#scopedModels: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
@@ -391,7 +439,7 @@ export class AgentSession {
|
|
|
391
439
|
#retryAttempt = 0;
|
|
392
440
|
#retryPromise: Promise<void> | undefined = undefined;
|
|
393
441
|
#retryResolve: (() => void) | undefined = undefined;
|
|
394
|
-
|
|
442
|
+
#activeRetryFallback: ActiveRetryFallbackState | undefined = undefined;
|
|
395
443
|
// Todo completion reminder state
|
|
396
444
|
#todoReminderCount = 0;
|
|
397
445
|
#todoPhases: TodoPhase[] = [];
|
|
@@ -478,6 +526,7 @@ export class AgentSession {
|
|
|
478
526
|
this.#customCommands = config.customCommands ?? [];
|
|
479
527
|
this.#skillsSettings = config.skillsSettings;
|
|
480
528
|
this.#modelRegistry = config.modelRegistry;
|
|
529
|
+
this.#validateRetryFallbackChains();
|
|
481
530
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
482
531
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
483
532
|
this.#onPayload = config.onPayload;
|
|
@@ -787,6 +836,13 @@ export class AgentSession {
|
|
|
787
836
|
assistantMsg.stopReason !== "aborted" &&
|
|
788
837
|
this.#retryAttempt > 0
|
|
789
838
|
) {
|
|
839
|
+
if (this.#activeRetryFallback && this.model) {
|
|
840
|
+
await this.#emitSessionEvent({
|
|
841
|
+
type: "retry_fallback_succeeded",
|
|
842
|
+
model: formatRetryFallbackSelector(this.model, this.thinkingLevel),
|
|
843
|
+
role: this.#activeRetryFallback.role,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
790
846
|
await this.#emitSessionEvent({
|
|
791
847
|
type: "auto_retry_end",
|
|
792
848
|
success: true,
|
|
@@ -985,6 +1041,7 @@ export class AgentSession {
|
|
|
985
1041
|
return;
|
|
986
1042
|
}
|
|
987
1043
|
try {
|
|
1044
|
+
await this.#maybeRestoreRetryFallbackPrimary();
|
|
988
1045
|
await this.agent.continue();
|
|
989
1046
|
} catch {
|
|
990
1047
|
options?.onError?.();
|
|
@@ -2288,6 +2345,8 @@ export class AgentSession {
|
|
|
2288
2345
|
// Reset todo reminder count on new user prompt
|
|
2289
2346
|
this.#todoReminderCount = 0;
|
|
2290
2347
|
|
|
2348
|
+
await this.#maybeRestoreRetryFallbackPrimary();
|
|
2349
|
+
|
|
2291
2350
|
// Validate model
|
|
2292
2351
|
if (!this.model) {
|
|
2293
2352
|
throw new Error(
|
|
@@ -3108,6 +3167,7 @@ export class AgentSession {
|
|
|
3108
3167
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
3109
3168
|
}
|
|
3110
3169
|
|
|
3170
|
+
this.#clearActiveRetryFallback();
|
|
3111
3171
|
this.#setModelWithProviderSessionReset(model);
|
|
3112
3172
|
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, role);
|
|
3113
3173
|
this.settings.setModelRole(role, this.#formatRoleModelValue(role, model));
|
|
@@ -3128,6 +3188,7 @@ export class AgentSession {
|
|
|
3128
3188
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
3129
3189
|
}
|
|
3130
3190
|
|
|
3191
|
+
this.#clearActiveRetryFallback();
|
|
3131
3192
|
this.#setModelWithProviderSessionReset(model);
|
|
3132
3193
|
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "temporary");
|
|
3133
3194
|
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
@@ -3253,6 +3314,7 @@ export class AgentSession {
|
|
|
3253
3314
|
const next = scopedModels[nextIndex];
|
|
3254
3315
|
|
|
3255
3316
|
// Apply model
|
|
3317
|
+
this.#clearActiveRetryFallback();
|
|
3256
3318
|
this.#setModelWithProviderSessionReset(next.model);
|
|
3257
3319
|
this.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
|
|
3258
3320
|
this.settings.setModelRole("default", this.#formatRoleModelValue("default", next.model));
|
|
@@ -3281,11 +3343,11 @@ export class AgentSession {
|
|
|
3281
3343
|
throw new Error(`No API key for ${nextModel.provider}/${nextModel.id}`);
|
|
3282
3344
|
}
|
|
3283
3345
|
|
|
3346
|
+
this.#clearActiveRetryFallback();
|
|
3284
3347
|
this.#setModelWithProviderSessionReset(nextModel);
|
|
3285
3348
|
this.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
|
|
3286
3349
|
this.settings.setModelRole("default", this.#formatRoleModelValue("default", nextModel));
|
|
3287
3350
|
this.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
|
|
3288
|
-
|
|
3289
3351
|
// Re-apply the current thinking level for the newly selected model
|
|
3290
3352
|
this.setThinkingLevel(this.thinkingLevel);
|
|
3291
3353
|
|
|
@@ -4768,6 +4830,217 @@ export class AgentSession {
|
|
|
4768
4830
|
);
|
|
4769
4831
|
}
|
|
4770
4832
|
|
|
4833
|
+
#getRetryFallbackChains(): RetryFallbackChains {
|
|
4834
|
+
const configuredChains = this.settings.get("retry.fallbackChains");
|
|
4835
|
+
if (!configuredChains || typeof configuredChains !== "object") return {};
|
|
4836
|
+
return configuredChains as RetryFallbackChains;
|
|
4837
|
+
}
|
|
4838
|
+
|
|
4839
|
+
#validateRetryFallbackChains(): void {
|
|
4840
|
+
const configuredChains = this.settings.get("retry.fallbackChains");
|
|
4841
|
+
if (configuredChains === undefined) return;
|
|
4842
|
+
if (!configuredChains || typeof configuredChains !== "object" || Array.isArray(configuredChains)) {
|
|
4843
|
+
const msg = "retry.fallbackChains must be a mapping of role names to selector arrays.";
|
|
4844
|
+
logger.warn(msg);
|
|
4845
|
+
this.configWarnings.push(msg);
|
|
4846
|
+
return;
|
|
4847
|
+
}
|
|
4848
|
+
|
|
4849
|
+
for (const [role, chain] of Object.entries(configuredChains)) {
|
|
4850
|
+
if (!Array.isArray(chain)) {
|
|
4851
|
+
const msg = `Fallback chain for role '${role}' must be an array of selector strings.`;
|
|
4852
|
+
logger.warn(msg);
|
|
4853
|
+
this.configWarnings.push(msg);
|
|
4854
|
+
continue;
|
|
4855
|
+
}
|
|
4856
|
+
for (const selectorStr of chain) {
|
|
4857
|
+
if (typeof selectorStr !== "string") {
|
|
4858
|
+
const msg = `Fallback chain for role '${role}' contains a non-string selector.`;
|
|
4859
|
+
logger.warn(msg);
|
|
4860
|
+
this.configWarnings.push(msg);
|
|
4861
|
+
continue;
|
|
4862
|
+
}
|
|
4863
|
+
const parsed = parseRetryFallbackSelector(selectorStr);
|
|
4864
|
+
if (!parsed) {
|
|
4865
|
+
const msg = `Invalid fallback selector format in role '${role}': ${selectorStr}`;
|
|
4866
|
+
logger.warn(msg);
|
|
4867
|
+
this.configWarnings.push(msg);
|
|
4868
|
+
continue;
|
|
4869
|
+
}
|
|
4870
|
+
const exists = this.#modelRegistry.find(parsed.provider, parsed.id);
|
|
4871
|
+
if (!exists) {
|
|
4872
|
+
const msg = `Fallback chain for role '${role}' references unknown model: ${selectorStr}`;
|
|
4873
|
+
logger.warn(msg);
|
|
4874
|
+
this.configWarnings.push(msg);
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
|
|
4880
|
+
#getRetryFallbackRevertPolicy(): RetryFallbackRevertPolicy {
|
|
4881
|
+
return this.settings.get("retry.fallbackRevertPolicy") === "never" ? "never" : "cooldown-expiry";
|
|
4882
|
+
}
|
|
4883
|
+
|
|
4884
|
+
#getRetryFallbackPrimarySelector(role: string): RetryFallbackSelector | undefined {
|
|
4885
|
+
const configuredSelector = this.settings.getModelRole(role);
|
|
4886
|
+
return configuredSelector ? parseRetryFallbackSelector(configuredSelector) : undefined;
|
|
4887
|
+
}
|
|
4888
|
+
|
|
4889
|
+
#clearActiveRetryFallback(): void {
|
|
4890
|
+
this.#activeRetryFallback = undefined;
|
|
4891
|
+
}
|
|
4892
|
+
|
|
4893
|
+
#isRetryFallbackSelectorSuppressed(selector: RetryFallbackSelector): boolean {
|
|
4894
|
+
return this.#modelRegistry.isSelectorSuppressed(selector.raw);
|
|
4895
|
+
}
|
|
4896
|
+
|
|
4897
|
+
#noteRetryFallbackCooldown(currentSelector: string, retryAfterMs: number | undefined, errorMessage: string): void {
|
|
4898
|
+
let cooldownMs = retryAfterMs;
|
|
4899
|
+
if (!cooldownMs || cooldownMs <= 0) {
|
|
4900
|
+
const reason = parseRateLimitReason(errorMessage);
|
|
4901
|
+
cooldownMs = reason === "UNKNOWN" ? 5 * 60 * 1000 : calculateRateLimitBackoffMs(reason);
|
|
4902
|
+
}
|
|
4903
|
+
this.#modelRegistry.suppressSelector(currentSelector, Date.now() + cooldownMs);
|
|
4904
|
+
}
|
|
4905
|
+
|
|
4906
|
+
#resolveRetryFallbackRole(currentSelector: string): string | undefined {
|
|
4907
|
+
const parsedCurrent = parseRetryFallbackSelector(currentSelector);
|
|
4908
|
+
if (!parsedCurrent) return undefined;
|
|
4909
|
+
const currentBaseSelector = formatRetryFallbackBaseSelector(parsedCurrent);
|
|
4910
|
+
for (const role of Object.keys(this.#getRetryFallbackChains())) {
|
|
4911
|
+
const primarySelector = this.#getRetryFallbackPrimarySelector(role);
|
|
4912
|
+
if (!primarySelector) continue;
|
|
4913
|
+
if (primarySelector.raw === currentSelector) return role;
|
|
4914
|
+
if (formatRetryFallbackBaseSelector(primarySelector) === currentBaseSelector) return role;
|
|
4915
|
+
}
|
|
4916
|
+
return undefined;
|
|
4917
|
+
}
|
|
4918
|
+
|
|
4919
|
+
#getRetryFallbackEffectiveChain(role: string): RetryFallbackSelector[] {
|
|
4920
|
+
const primarySelector = this.#getRetryFallbackPrimarySelector(role);
|
|
4921
|
+
if (!primarySelector) return [];
|
|
4922
|
+
const chain = [primarySelector];
|
|
4923
|
+
const seen = new Set<string>([primarySelector.raw]);
|
|
4924
|
+
for (const selector of this.#getRetryFallbackChains()[role] ?? []) {
|
|
4925
|
+
const parsed = parseRetryFallbackSelector(selector);
|
|
4926
|
+
if (!parsed || seen.has(parsed.raw)) continue;
|
|
4927
|
+
seen.add(parsed.raw);
|
|
4928
|
+
chain.push(parsed);
|
|
4929
|
+
}
|
|
4930
|
+
return chain;
|
|
4931
|
+
}
|
|
4932
|
+
|
|
4933
|
+
#findRetryFallbackCandidates(role: string, currentSelector: string): RetryFallbackSelector[] {
|
|
4934
|
+
const chain = this.#getRetryFallbackEffectiveChain(role);
|
|
4935
|
+
if (chain.length <= 1) return [];
|
|
4936
|
+
const parsedCurrent = parseRetryFallbackSelector(currentSelector);
|
|
4937
|
+
const currentBaseSelector = parsedCurrent ? formatRetryFallbackBaseSelector(parsedCurrent) : undefined;
|
|
4938
|
+
const exactIndex = chain.findIndex(selector => selector.raw === currentSelector);
|
|
4939
|
+
if (exactIndex >= 0) return chain.slice(exactIndex + 1);
|
|
4940
|
+
const baseIndex = currentBaseSelector
|
|
4941
|
+
? chain.findIndex(selector => formatRetryFallbackBaseSelector(selector) === currentBaseSelector)
|
|
4942
|
+
: -1;
|
|
4943
|
+
if (baseIndex >= 0) return chain.slice(baseIndex + 1);
|
|
4944
|
+
return chain.slice(1);
|
|
4945
|
+
}
|
|
4946
|
+
|
|
4947
|
+
async #applyRetryFallbackCandidate(
|
|
4948
|
+
role: string,
|
|
4949
|
+
selector: RetryFallbackSelector,
|
|
4950
|
+
currentSelector: string,
|
|
4951
|
+
): Promise<void> {
|
|
4952
|
+
const candidate = this.#modelRegistry.find(selector.provider, selector.id);
|
|
4953
|
+
if (!candidate) {
|
|
4954
|
+
throw new Error(`Retry fallback model not found: ${selector.raw}`);
|
|
4955
|
+
}
|
|
4956
|
+
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
4957
|
+
if (!apiKey) {
|
|
4958
|
+
throw new Error(`No API key for retry fallback ${selector.raw}`);
|
|
4959
|
+
}
|
|
4960
|
+
|
|
4961
|
+
const currentThinkingLevel = this.thinkingLevel;
|
|
4962
|
+
const nextThinkingLevel = selector.thinkingLevel ?? currentThinkingLevel;
|
|
4963
|
+
|
|
4964
|
+
this.#setModelWithProviderSessionReset(candidate);
|
|
4965
|
+
this.sessionManager.appendModelChange(`${candidate.provider}/${candidate.id}`, "temporary");
|
|
4966
|
+
this.settings.getStorage()?.recordModelUsage(`${candidate.provider}/${candidate.id}`);
|
|
4967
|
+
this.setThinkingLevel(nextThinkingLevel);
|
|
4968
|
+
if (!this.#activeRetryFallback) {
|
|
4969
|
+
this.#activeRetryFallback = {
|
|
4970
|
+
role,
|
|
4971
|
+
originalSelector: currentSelector,
|
|
4972
|
+
originalThinkingLevel: currentThinkingLevel,
|
|
4973
|
+
lastAppliedFallbackThinkingLevel: nextThinkingLevel,
|
|
4974
|
+
};
|
|
4975
|
+
} else {
|
|
4976
|
+
this.#activeRetryFallback.lastAppliedFallbackThinkingLevel = nextThinkingLevel;
|
|
4977
|
+
}
|
|
4978
|
+
await this.#emitSessionEvent({
|
|
4979
|
+
type: "retry_fallback_applied",
|
|
4980
|
+
from: currentSelector,
|
|
4981
|
+
to: selector.raw,
|
|
4982
|
+
role,
|
|
4983
|
+
});
|
|
4984
|
+
}
|
|
4985
|
+
|
|
4986
|
+
async #tryRetryModelFallback(currentSelector: string): Promise<boolean> {
|
|
4987
|
+
const role = this.#activeRetryFallback?.role ?? this.#resolveRetryFallbackRole(currentSelector);
|
|
4988
|
+
if (!role) return false;
|
|
4989
|
+
|
|
4990
|
+
for (const selector of this.#findRetryFallbackCandidates(role, currentSelector)) {
|
|
4991
|
+
if (this.#isRetryFallbackSelectorSuppressed(selector)) continue;
|
|
4992
|
+
const candidate = this.#modelRegistry.find(selector.provider, selector.id);
|
|
4993
|
+
if (!candidate) continue;
|
|
4994
|
+
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
4995
|
+
if (!apiKey) continue;
|
|
4996
|
+
await this.#applyRetryFallbackCandidate(role, selector, currentSelector);
|
|
4997
|
+
return true;
|
|
4998
|
+
}
|
|
4999
|
+
|
|
5000
|
+
return false;
|
|
5001
|
+
}
|
|
5002
|
+
|
|
5003
|
+
async #maybeRestoreRetryFallbackPrimary(): Promise<void> {
|
|
5004
|
+
if (!this.#activeRetryFallback) return;
|
|
5005
|
+
if (this.#getRetryFallbackRevertPolicy() !== "cooldown-expiry") return;
|
|
5006
|
+
|
|
5007
|
+
const {
|
|
5008
|
+
originalSelector: originalSelectorRaw,
|
|
5009
|
+
originalThinkingLevel,
|
|
5010
|
+
lastAppliedFallbackThinkingLevel,
|
|
5011
|
+
} = this.#activeRetryFallback;
|
|
5012
|
+
const originalSelector = parseRetryFallbackSelector(originalSelectorRaw);
|
|
5013
|
+
if (!originalSelector) {
|
|
5014
|
+
this.#clearActiveRetryFallback();
|
|
5015
|
+
return;
|
|
5016
|
+
}
|
|
5017
|
+
|
|
5018
|
+
const currentModel = this.model;
|
|
5019
|
+
if (!currentModel) return;
|
|
5020
|
+
const currentSelector = formatRetryFallbackSelector(currentModel, this.thinkingLevel);
|
|
5021
|
+
if (currentSelector === originalSelector.raw) {
|
|
5022
|
+
if (!this.#isRetryFallbackSelectorSuppressed(originalSelector)) {
|
|
5023
|
+
this.#clearActiveRetryFallback();
|
|
5024
|
+
}
|
|
5025
|
+
return;
|
|
5026
|
+
}
|
|
5027
|
+
if (this.#isRetryFallbackSelectorSuppressed(originalSelector)) return;
|
|
5028
|
+
|
|
5029
|
+
const primaryModel = this.#modelRegistry.find(originalSelector.provider, originalSelector.id);
|
|
5030
|
+
if (!primaryModel) return;
|
|
5031
|
+
const apiKey = await this.#modelRegistry.getApiKey(primaryModel, this.sessionId);
|
|
5032
|
+
if (!apiKey) return;
|
|
5033
|
+
|
|
5034
|
+
const currentThinkingLevel = this.thinkingLevel;
|
|
5035
|
+
const thinkingToApply =
|
|
5036
|
+
currentThinkingLevel === lastAppliedFallbackThinkingLevel ? originalThinkingLevel : currentThinkingLevel;
|
|
5037
|
+
this.#setModelWithProviderSessionReset(primaryModel);
|
|
5038
|
+
this.sessionManager.appendModelChange(`${primaryModel.provider}/${primaryModel.id}`, "temporary");
|
|
5039
|
+
this.settings.getStorage()?.recordModelUsage(`${primaryModel.provider}/${primaryModel.id}`);
|
|
5040
|
+
this.setThinkingLevel(thinkingToApply);
|
|
5041
|
+
this.#clearActiveRetryFallback();
|
|
5042
|
+
}
|
|
5043
|
+
|
|
4771
5044
|
#parseRetryAfterMsFromError(errorMessage: string): number | undefined {
|
|
4772
5045
|
const now = Date.now();
|
|
4773
5046
|
const retryAfterMsMatch = /retry-after-ms\s*[:=]\s*(\d+)/i.exec(errorMessage);
|
|
@@ -4847,12 +5120,13 @@ export class AgentSession {
|
|
|
4847
5120
|
}
|
|
4848
5121
|
|
|
4849
5122
|
const errorMessage = message.errorMessage || "Unknown error";
|
|
5123
|
+
const parsedRetryAfterMs = this.#parseRetryAfterMsFromError(errorMessage);
|
|
4850
5124
|
let delayMs = retrySettings.baseDelayMs * 2 ** (this.#retryAttempt - 1);
|
|
5125
|
+
let switchedCredential = false;
|
|
5126
|
+
let switchedModel = false;
|
|
4851
5127
|
|
|
4852
5128
|
if (this.model && isUsageLimitError(errorMessage)) {
|
|
4853
|
-
const retryAfterMs =
|
|
4854
|
-
this.#parseRetryAfterMsFromError(errorMessage) ??
|
|
4855
|
-
calculateRateLimitBackoffMs(parseRateLimitReason(errorMessage));
|
|
5129
|
+
const retryAfterMs = parsedRetryAfterMs ?? calculateRateLimitBackoffMs(parseRateLimitReason(errorMessage));
|
|
4856
5130
|
const switched = await this.#modelRegistry.authStorage.markUsageLimitReached(
|
|
4857
5131
|
this.model.provider,
|
|
4858
5132
|
this.sessionId,
|
|
@@ -4862,6 +5136,7 @@ export class AgentSession {
|
|
|
4862
5136
|
},
|
|
4863
5137
|
);
|
|
4864
5138
|
if (switched) {
|
|
5139
|
+
switchedCredential = true;
|
|
4865
5140
|
delayMs = 0;
|
|
4866
5141
|
} else if (retryAfterMs > delayMs) {
|
|
4867
5142
|
// No more accounts to switch to — wait out the backoff
|
|
@@ -4869,6 +5144,17 @@ export class AgentSession {
|
|
|
4869
5144
|
}
|
|
4870
5145
|
}
|
|
4871
5146
|
|
|
5147
|
+
const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
|
|
5148
|
+
if (!switchedCredential && currentSelector) {
|
|
5149
|
+
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
5150
|
+
switchedModel = await this.#tryRetryModelFallback(currentSelector);
|
|
5151
|
+
if (switchedModel) {
|
|
5152
|
+
delayMs = 0;
|
|
5153
|
+
} else if (parsedRetryAfterMs && parsedRetryAfterMs > delayMs) {
|
|
5154
|
+
delayMs = parsedRetryAfterMs;
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
|
|
4872
5158
|
await this.#emitSessionEvent({
|
|
4873
5159
|
type: "auto_retry_start",
|
|
4874
5160
|
attempt: this.#retryAttempt,
|