fix(task): preserved deferred fallback chains
Install subagent retry fallback chains after deferred model patterns resolve so runtime-only candidates keep their ordered fallbacks.\n\nFixes #4421
This commit is contained in:
@@ -33,7 +33,9 @@ import { shouldEnableAppendOnlyContext } from "./config/append-only-context-mode
|
||||
import { shouldInlineToolDescriptors } from "./config/inline-tool-descriptors-mode";
|
||||
import { isAuthenticated, kNoAuth, ModelRegistry } from "./config/model-registry";
|
||||
import {
|
||||
formatModelSelectorValue,
|
||||
formatModelString,
|
||||
formatModelStringWithRouting,
|
||||
getModelMatchPreferences,
|
||||
parseModelPattern,
|
||||
parseModelString,
|
||||
@@ -396,6 +398,8 @@ export interface CreateAgentSessionOptions {
|
||||
modelPattern?: string | string[];
|
||||
/** Authenticated fallback selector for deferred subagent model patterns. */
|
||||
modelPatternAuthFallback?: string;
|
||||
/** Role name used to install retry fallbacks after deferred subagent patterns resolve. */
|
||||
modelPatternFallbackRole?: string;
|
||||
/** Thinking selector. Default: from settings, else unset */
|
||||
thinkingLevel?: ConfiguredThinkingLevel;
|
||||
/** Models available for cycling (Ctrl+P in interactive mode) */
|
||||
@@ -1968,12 +1972,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
if (!model && deferredModelPatterns.length > 0) {
|
||||
const availableModels = modelRegistry.getAll();
|
||||
const matchPreferences = getModelMatchPreferences(settings);
|
||||
for (const pattern of deferredModelPatterns) {
|
||||
for (let patternIndex = 0; patternIndex < deferredModelPatterns.length; patternIndex += 1) {
|
||||
const pattern = deferredModelPatterns[patternIndex];
|
||||
const primary = parseModelPattern(pattern, availableModels, matchPreferences);
|
||||
if (!primary.model) continue;
|
||||
let selectedModel = primary.model;
|
||||
let selectedThinkingLevel = primary.thinkingLevel;
|
||||
let selectedExplicitThinkingLevel = primary.explicitThinkingLevel;
|
||||
let authFallbackUsed = false;
|
||||
if (options.modelPatternAuthFallback) {
|
||||
const primaryKey = await modelRegistry.getApiKey(primary.model);
|
||||
if (primaryKey !== kNoAuth && !isAuthenticated(primaryKey)) {
|
||||
@@ -1988,10 +1994,52 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
selectedModel = fallback.model;
|
||||
selectedThinkingLevel = fallback.thinkingLevel;
|
||||
selectedExplicitThinkingLevel = fallback.explicitThinkingLevel;
|
||||
authFallbackUsed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!authFallbackUsed && options.modelPatternFallbackRole) {
|
||||
const primarySelector = formatModelSelectorValue(
|
||||
formatModelStringWithRouting(primary.model),
|
||||
primary.thinkingLevel,
|
||||
);
|
||||
const seenSelectors = new Set<string>([primarySelector]);
|
||||
const fallbackSelectors: string[] = [];
|
||||
for (const fallbackPattern of deferredModelPatterns.slice(patternIndex + 1)) {
|
||||
const fallback = parseModelPattern(fallbackPattern, availableModels, matchPreferences);
|
||||
if (!fallback.model) continue;
|
||||
const fallbackSelector = formatModelSelectorValue(
|
||||
formatModelStringWithRouting(fallback.model),
|
||||
fallback.thinkingLevel,
|
||||
);
|
||||
if (seenSelectors.has(fallbackSelector)) continue;
|
||||
seenSelectors.add(fallbackSelector);
|
||||
fallbackSelectors.push(fallbackSelector);
|
||||
}
|
||||
if (fallbackSelectors.length > 0) {
|
||||
const modelRoles: Record<string, string> = {};
|
||||
const existingRoles = settings.getModelRoles();
|
||||
for (const role in existingRoles) {
|
||||
const selector = existingRoles[role];
|
||||
if (selector) {
|
||||
modelRoles[role] = selector;
|
||||
}
|
||||
}
|
||||
modelRoles[options.modelPatternFallbackRole] = primarySelector;
|
||||
settings.override("modelRoles", modelRoles);
|
||||
const fallbackChains: Record<string, string[]> = {
|
||||
[options.modelPatternFallbackRole]: fallbackSelectors,
|
||||
};
|
||||
const existingFallbackChains = settings.get("retry.fallbackChains");
|
||||
for (const role in existingFallbackChains) {
|
||||
if (role !== options.modelPatternFallbackRole) {
|
||||
fallbackChains[role] = existingFallbackChains[role];
|
||||
}
|
||||
}
|
||||
settings.override("retry.fallbackChains", fallbackChains);
|
||||
}
|
||||
}
|
||||
model = selectedModel;
|
||||
modelFallbackMessage = undefined;
|
||||
if (selectedExplicitThinkingLevel) {
|
||||
|
||||
@@ -2164,6 +2164,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
modelPattern: model || modelOverride === undefined ? undefined : modelPatterns,
|
||||
modelPatternAuthFallback:
|
||||
model || modelOverride === undefined ? undefined : options.parentActiveModelPattern,
|
||||
modelPatternFallbackRole:
|
||||
model || modelOverride === undefined ? undefined : `${SUBAGENT_RETRY_FALLBACK_ROLE_PREFIX}${id}`,
|
||||
thinkingLevel: effectiveThinkingLevel,
|
||||
toolNames,
|
||||
outputSchema,
|
||||
|
||||
@@ -154,11 +154,13 @@ describe("subagent runtime model resolution", () => {
|
||||
let childModel: Model | undefined;
|
||||
let childModelPattern: unknown;
|
||||
let childModelPatternAuthFallback: unknown;
|
||||
let childModelPatternFallbackRole: unknown;
|
||||
vi.spyOn(sdkModule, "createAgentSession").mockImplementation(async options => {
|
||||
if (!options) throw new Error("Expected createAgentSession options");
|
||||
childModel = options.model;
|
||||
childModelPattern = options.modelPattern;
|
||||
childModelPatternAuthFallback = options.modelPatternAuthFallback;
|
||||
childModelPatternFallbackRole = options.modelPatternFallbackRole;
|
||||
return { session: createYieldingSession(), extensionsResult: {}, setToolUIContext: () => {} } as never;
|
||||
});
|
||||
|
||||
@@ -183,5 +185,6 @@ describe("subagent runtime model resolution", () => {
|
||||
expect(childModel).toBeUndefined();
|
||||
expect(childModelPattern).toEqual(["openai-codex/gpt-5.5:auto"]);
|
||||
expect(childModelPatternAuthFallback).toBe("openai-codex/gpt-5.5");
|
||||
expect(childModelPatternFallbackRole).toBe("subagent:issue-4421");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,7 +80,7 @@ describe("createAgentSession deferred model pattern resolution", () => {
|
||||
pi.registerProvider("runtime-provider", dynamicOnlyProviderConfig);
|
||||
};
|
||||
|
||||
async function buildSessionOptions(modelPattern: string) {
|
||||
async function buildSessionOptions(modelPattern: string | string[]) {
|
||||
// Pass an explicit ModelRegistry so createAgentSession skips its implicit
|
||||
// ModelRegistry.refreshInBackground() — a network model-discovery pass
|
||||
// (~250ms/session) that contributes nothing here: the model resolves from
|
||||
@@ -206,6 +206,24 @@ describe("createAgentSession deferred model pattern resolution", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("installs fallback chain for remaining deferred subagent modelPattern candidates", async () => {
|
||||
const { session } = await createAgentSession({
|
||||
...(await buildSessionOptions(["runtime-provider/runtime-model", "runtime-provider/runtime-reasoning-model"])),
|
||||
modelPatternFallbackRole: "subagent:deferred",
|
||||
});
|
||||
|
||||
try {
|
||||
expect(session.model?.provider).toBe("runtime-provider");
|
||||
expect(session.model?.id).toBe("runtime-model");
|
||||
expect(session.settings.getModelRole("subagent:deferred")).toBe("runtime-provider/runtime-model");
|
||||
expect(session.settings.get("retry.fallbackChains")["subagent:deferred"]).toEqual([
|
||||
"runtime-provider/runtime-reasoning-model",
|
||||
]);
|
||||
} finally {
|
||||
await session.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test("does not apply default role thinking override when modelPattern is explicit", async () => {
|
||||
const settings = Settings.isolated({ defaultThinkingLevel: "off" });
|
||||
settings.setModelRole("smol", "runtime-provider/runtime-reasoning-model");
|
||||
|
||||
Reference in New Issue
Block a user