Skip to content

Commit e4f9eb9

Browse files
wings1029claude
andcommitted
fix(agents): prevent indefinite session model pinning from polluted fallback origin
resolveAutoFallbackPrimaryProbe required the persisted modelOverrideFallbackOrigin to match the current configured primary before firing the snap-back probe. When the origin fields were polluted with the failing model instead of the configured primary (#92776), the guard returned undefined and the probe never fired — leaving the session permanently stuck on a degraded fallback model. Fix: use the current configured primary as the probe origin instead of requiring a matching persisted origin. This ensures the snap-back probe always fires, attempting to clear the auto-fallback override once the primary becomes available. Also relax entryMatchesAutoFallbackPrimaryProbe to match on the fallback override alone (provider/model) without requiring the persisted origin to match — polluted session entries must still be updatable after the probe fires. Test: updated 'skips primary probes for strict or stale fallback selections' — when origin differs from primary, the probe now returns a valid probe targeting the current primary instead of undefined. Fixes #92776. Co-Authored-By: Claude <[email protected]>
1 parent 8682d07 commit e4f9eb9

2 files changed

Lines changed: 27 additions & 15 deletions

File tree

src/agents/agent-scope.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,12 @@ describe("resolveAgentConfig", () => {
663663
primaryModel: "gpt-5.4",
664664
probeState: new Map(),
665665
}),
666-
).toBeUndefined();
666+
).toEqual({
667+
provider: "openai",
668+
model: "gpt-5.4",
669+
fallbackProvider: "google",
670+
fallbackModel: "gemini-3-pro",
671+
});
667672
expect(
668673
resolveAutoFallbackPrimaryProbe({
669674
entry: {

src/agents/agent-scope.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -151,25 +151,29 @@ export function resolveAutoFallbackPrimaryProbe(params: {
151151
return undefined;
152152
}
153153

154-
const originProvider = normalizeOptionalString(entry.modelOverrideFallbackOriginProvider);
155-
const originModel = normalizeOptionalString(entry.modelOverrideFallbackOriginModel);
156154
const overrideProvider = normalizeOptionalString(entry.providerOverride);
157155
const overrideModel = normalizeOptionalString(entry.modelOverride);
158156
const primaryProvider = normalizeOptionalString(params.primaryProvider);
159157
const primaryModel = normalizeOptionalString(params.primaryModel);
160-
if (!originProvider || !originModel || !overrideProvider || !overrideModel) {
158+
if (!overrideProvider || !overrideModel) {
161159
return undefined;
162160
}
163161
if (!primaryProvider || !primaryModel) {
164162
return undefined;
165163
}
166-
if (originProvider !== primaryProvider || originModel !== primaryModel) {
167-
return undefined;
168-
}
169-
if (overrideProvider === originProvider && overrideModel === originModel) {
164+
if (overrideProvider === primaryProvider && overrideModel === primaryModel) {
170165
return undefined;
171166
}
172167

168+
// Use the current configured primary as the probe origin rather than
169+
// requiring a persisted origin that matches. The persisted origin
170+
// can be missing (first fallback), polluted with the failing model
171+
// instead of the primary (#92776), or stale (primary changed).
172+
// Using the current primary ensures the snap-back probe fires and
173+
// clears the auto-fallback override once the primary is available.
174+
const probeOriginProvider = primaryProvider;
175+
const probeOriginModel = primaryModel;
176+
173177
const now = params.now ?? Date.now();
174178
const minIntervalMs = params.minIntervalMs ?? AUTO_FALLBACK_PRIMARY_PROBE_INTERVAL_MS;
175179
const state = params.probeState ?? autoFallbackPrimaryProbeState;
@@ -181,8 +185,8 @@ export function resolveAutoFallbackPrimaryProbe(params: {
181185
});
182186
const key = autoFallbackPrimaryProbeStateKey({
183187
sessionKey: params.sessionKey,
184-
primaryProvider: originProvider,
185-
primaryModel: originModel,
188+
primaryProvider: probeOriginProvider,
189+
primaryModel: probeOriginModel,
186190
});
187191
const lastProbeAt = state.get(key);
188192
if (
@@ -197,8 +201,8 @@ export function resolveAutoFallbackPrimaryProbe(params: {
197201
entry.authProfileOverrideSource ??
198202
(entry.authProfileOverrideCompactionCount !== undefined ? "auto" : undefined);
199203
return {
200-
provider: originProvider,
201-
model: originModel,
204+
provider: probeOriginProvider,
205+
model: probeOriginModel,
202206
fallbackProvider: overrideProvider,
203207
fallbackModel: overrideModel,
204208
...(fallbackAuthProfileId
@@ -263,11 +267,14 @@ export function entryMatchesAutoFallbackPrimaryProbe(
263267
if (entry.modelOverrideSource !== "auto" && !recoveredAutoFallbackOverride) {
264268
return false;
265269
}
270+
// Match on the fallback override (the model the session was stuck on).
271+
// We intentionally do NOT require the persisted origin to match the
272+
// probe origin — the origin can be polluted (#92776) and the probe
273+
// now always uses the current configured primary. Matching only on
274+
// the override ensures polluted session entries can still be updated.
266275
return (
267276
normalizeOptionalString(entry.providerOverride) === probe.fallbackProvider &&
268-
normalizeOptionalString(entry.modelOverride) === probe.fallbackModel &&
269-
normalizeOptionalString(entry.modelOverrideFallbackOriginProvider) === probe.provider &&
270-
normalizeOptionalString(entry.modelOverrideFallbackOriginModel) === probe.model
277+
normalizeOptionalString(entry.modelOverride) === probe.fallbackModel
271278
);
272279
}
273280

0 commit comments

Comments
 (0)