diff --git a/src/resources/extensions/gsd/auto-loop.ts b/src/resources/extensions/gsd/auto-loop.ts index 3b333ba95..9ba92275f 100644 --- a/src/resources/extensions/gsd/auto-loop.ts +++ b/src/resources/extensions/gsd/auto-loop.ts @@ -456,6 +456,7 @@ export interface LoopDeps { prefs: GSDPreferences | undefined, verbose: boolean, startModel: { provider: string; id: string } | null, + retryContext?: { isRetry: boolean; previousTier?: string }, ) => Promise<{ routing: { tier: string; modelDowngraded: boolean } | null }>; startUnitSupervision: (sctx: { s: AutoSession; @@ -1182,6 +1183,14 @@ export async function autoLoop( unitId, }); + // Detect retry and capture previous tier for escalation + const isRetry = !!( + s.currentUnit && + s.currentUnit.type === unitType && + s.currentUnit.id === unitId + ); + const previousTier = s.currentUnitRouting?.tier; + // Closeout previous unit if (s.currentUnit) { await deps.closeoutUnit( @@ -1335,7 +1344,7 @@ export async function autoLoop( ); } - // Select and apply model + // Select and apply model (with tier escalation on retry) const modelResult = await deps.selectAndApplyModel( ctx, pi, @@ -1345,6 +1354,7 @@ export async function autoLoop( prefs, s.verbose, s.autoModeStartModel, + { isRetry, previousTier }, ); s.currentUnitRouting = modelResult.routing as AutoSession["currentUnitRouting"]; diff --git a/src/resources/extensions/gsd/auto-model-selection.ts b/src/resources/extensions/gsd/auto-model-selection.ts index 70be37671..70b2fa7e1 100644 --- a/src/resources/extensions/gsd/auto-model-selection.ts +++ b/src/resources/extensions/gsd/auto-model-selection.ts @@ -7,8 +7,9 @@ import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent"; import type { GSDPreferences } from "./preferences.js"; import { resolveModelWithFallbacksForUnit, resolveDynamicRoutingConfig } from "./preferences.js"; +import type { ComplexityTier } from "./complexity-classifier.js"; import { classifyUnitComplexity, tierLabel } from "./complexity-classifier.js"; -import { resolveModelForComplexity } from "./model-router.js"; +import { resolveModelForComplexity, escalateTier } from "./model-router.js"; import { getLedger, getProjectTotals } from "./metrics.js"; import { unitPhaseLabel } from "./auto-dashboard.js"; @@ -33,6 +34,7 @@ export async function selectAndApplyModel( prefs: GSDPreferences | undefined, verbose: boolean, autoModeStartModel: { provider: string; id: string } | null, + retryContext?: { isRetry: boolean; previousTier?: string }, ): Promise { const modelConfig = resolveModelWithFallbacksForUnit(unitType); let routing: { tier: string; modelDowngraded: boolean } | null = null; @@ -60,8 +62,27 @@ export async function selectAndApplyModel( const shouldClassify = !isHook || routingConfig.hooks !== false; if (shouldClassify) { - const classification = classifyUnitComplexity(unitType, unitId, basePath, budgetPct); + let classification = classifyUnitComplexity(unitType, unitId, basePath, budgetPct); const availableModelIds = availableModels.map(m => m.id); + + // Escalate tier on retry when escalate_on_failure is enabled (default: true) + if ( + retryContext?.isRetry && + retryContext.previousTier && + routingConfig.escalate_on_failure !== false + ) { + const escalated = escalateTier(retryContext.previousTier as ComplexityTier); + if (escalated) { + classification = { ...classification, tier: escalated, reason: "escalated after failure" }; + if (verbose) { + ctx.ui.notify( + `Tier escalation: ${retryContext.previousTier} → ${escalated} (retry after failure)`, + "info", + ); + } + } + } + const routingResult = resolveModelForComplexity(classification, modelConfig, routingConfig, availableModelIds); if (routingResult.wasDowngraded) {