@@ -37,8 +37,10 @@ const state = vi.hoisted(() => ({
3737 ) ,
3838 resolveEffectiveModelFallbacksMock : vi . fn ( ) . mockReturnValue ( undefined ) ,
3939 hasLegacyAutoFallbackWithoutOriginMock : vi . fn ( ( _entry : unknown ) => false ) ,
40+ isStaleAutoFallbackOriginOverrideMock : vi . fn ( ( _params : unknown ) => false ) ,
4041 resolveAutoFallbackPrimaryProbeMock : vi . fn ( ( _params : unknown ) => undefined as unknown ) ,
4142 resolveChannelModelOverrideMock : vi . fn ( ( _params : unknown ) => null as unknown ) ,
43+ applyModelOverrideToSessionEntryMock : vi . fn ( ( _params : unknown ) => ( { updated : false } ) ) ,
4244 assertLifecycleCurrentMock : vi . fn ( ) ,
4345 emitAgentEventMock : vi . fn ( ) ,
4446 registerAgentRunContextMock : vi . fn ( ) ,
@@ -346,7 +348,8 @@ vi.mock("../sessions/level-overrides.js", () => ({
346348} ) ) ;
347349
348350vi . mock ( "../sessions/model-overrides.js" , ( ) => ( {
349- applyModelOverrideToSessionEntry : ( ) => ( { updated : false } ) ,
351+ applyModelOverrideToSessionEntry : ( params : unknown ) =>
352+ state . applyModelOverrideToSessionEntryMock ( params ) ,
350353 repairProviderWrappedModelOverride : ( ) => ( { updated : false } ) ,
351354} ) ) ;
352355
@@ -384,6 +387,8 @@ vi.mock("./agent-scope.js", () => ({
384387 hasLegacyAutoFallbackWithoutOrigin : ( entry : unknown ) =>
385388 state . hasLegacyAutoFallbackWithoutOriginMock ( entry ) ,
386389 hasSessionAutoModelFallbackProvenance : ( ) => false ,
390+ isStaleAutoFallbackOriginOverride : ( params : unknown ) =>
391+ state . isStaleAutoFallbackOriginOverrideMock ( params ) ,
387392 listAgentEntries : ( ) => [ ] ,
388393 listAgentIds : ( ) => [ "default" ] ,
389394 markAutoFallbackPrimaryProbe : vi . fn ( ) ,
@@ -957,7 +962,9 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
957962 state . resolveAgentSkillsFilterMock . mockReturnValue ( undefined ) ;
958963 state . loadManifestModelCatalogMock . mockReturnValue ( [ ] ) ;
959964 state . hasLegacyAutoFallbackWithoutOriginMock . mockReturnValue ( false ) ;
965+ state . isStaleAutoFallbackOriginOverrideMock . mockReturnValue ( false ) ;
960966 state . resolveAutoFallbackPrimaryProbeMock . mockReturnValue ( undefined ) ;
967+ state . applyModelOverrideToSessionEntryMock . mockReturnValue ( { updated : false } ) ;
961968 state . resolveChannelModelOverrideMock . mockImplementation ( ( params : unknown ) => {
962969 const input = params as {
963970 cfg ?: { channels ?: { modelByChannel ?: Record < string , Record < string , string > > } } ;
@@ -1521,6 +1528,88 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
15211528 expect ( fallbackParams . model ) . toBe ( "channel-model" ) ;
15221529 } ) ;
15231530
1531+ it ( "clears stale auto fallback origin overrides before agent-command model selection" , async ( ) => {
1532+ setupSingleAttemptFallback ( ) ;
1533+ state . isStaleAutoFallbackOriginOverrideMock . mockReturnValue ( true ) ;
1534+ state . applyModelOverrideToSessionEntryMock . mockImplementation ( ( params : unknown ) => {
1535+ const entry = ( params as { entry : SessionEntry } ) . entry ;
1536+ delete entry . providerOverride ;
1537+ delete entry . modelOverride ;
1538+ delete entry . modelOverrideSource ;
1539+ delete entry . modelOverrideFallbackOriginProvider ;
1540+ delete entry . modelOverrideFallbackOriginModel ;
1541+ entry . updatedAt = 2 ;
1542+ return { updated : true } ;
1543+ } ) ;
1544+ state . runtimeConfigMock = {
1545+ agents : {
1546+ defaults : {
1547+ model : "anthropic/claude-opus-4-8" ,
1548+ } ,
1549+ } ,
1550+ } ;
1551+ const sessionEntry : SessionEntry = {
1552+ sessionId : "session-1" ,
1553+ updatedAt : 1 ,
1554+ providerOverride : "anthropic" ,
1555+ modelOverride : "claude-opus-4-7" ,
1556+ modelOverrideSource : "auto" ,
1557+ modelOverrideFallbackOriginProvider : "anthropic" ,
1558+ modelOverrideFallbackOriginModel : "claude-haiku-4-5" ,
1559+ skillsSnapshot : { prompt : "" , skills : [ ] , version : 0 } ,
1560+ } ;
1561+ const sessionStore : Record < string , SessionEntry > = { "agent:main:main" : sessionEntry } ;
1562+ state . sessionEntryMock = sessionEntry ;
1563+ state . sessionStoreMock = sessionStore ;
1564+ state . storePathMock = "/tmp/openclaw-session-store.json" ;
1565+ state . runAgentAttemptMock . mockResolvedValue ( makeSuccessResult ( "anthropic" , "claude-opus-4-8" ) ) ;
1566+
1567+ await runBasicAgentCommand ( ) ;
1568+
1569+ const staleOriginParams = requireRecord (
1570+ mockCallArg ( state . isStaleAutoFallbackOriginOverrideMock ) ,
1571+ "stale origin params" ,
1572+ ) ;
1573+ expect ( staleOriginParams . entry ) . toEqual ( expect . objectContaining ( { sessionId : "session-1" } ) ) ;
1574+ expectRecordFields ( staleOriginParams , {
1575+ primaryProvider : "anthropic" ,
1576+ primaryModel : "claude-opus-4-8" ,
1577+ } ) ;
1578+ const applyOverrideParams = requireRecord (
1579+ mockCallArg ( state . applyModelOverrideToSessionEntryMock ) ,
1580+ "apply model override params" ,
1581+ ) ;
1582+ expect ( applyOverrideParams . entry ) . toEqual ( expect . objectContaining ( { sessionId : "session-1" } ) ) ;
1583+ expectRecordFields ( applyOverrideParams , {
1584+ preserveAuthProfileOverride : true ,
1585+ } ) ;
1586+ expectRecordFields ( applyOverrideParams . selection , {
1587+ provider : "anthropic" ,
1588+ model : "claude-opus-4-8" ,
1589+ isDefault : true ,
1590+ } ) ;
1591+ const cleanupPersist = state . persistSessionEntryMock . mock . calls
1592+ . map (
1593+ ( [ params ] ) => params as { entry ?: SessionEntry ; sessionKey ?: string ; storePath ?: string } ,
1594+ )
1595+ . find ( ( params ) => params . entry ?. updatedAt === 2 ) ;
1596+ expect ( cleanupPersist ) . toEqual (
1597+ expect . objectContaining ( {
1598+ sessionKey : "agent:main:main" ,
1599+ storePath : "/tmp/openclaw-session-store.json" ,
1600+ } ) ,
1601+ ) ;
1602+ expect ( cleanupPersist ?. entry ?. providerOverride ) . toBeUndefined ( ) ;
1603+ expect ( cleanupPersist ?. entry ?. modelOverride ) . toBeUndefined ( ) ;
1604+ expect ( cleanupPersist ?. entry ?. modelOverrideSource ) . toBeUndefined ( ) ;
1605+ expect ( cleanupPersist ?. entry ?. modelOverrideFallbackOriginProvider ) . toBeUndefined ( ) ;
1606+ expect ( cleanupPersist ?. entry ?. modelOverrideFallbackOriginModel ) . toBeUndefined ( ) ;
1607+ const fallbackParams = mockCallArg ( state . runWithModelFallbackMock ) as FallbackRunnerParams ;
1608+ expect ( fallbackParams . provider ) . toBe ( "anthropic" ) ;
1609+ expect ( fallbackParams . model ) . toBe ( "claude-opus-4-8" ) ;
1610+ expect ( state . resolveAutoFallbackPrimaryProbeMock ) . not . toHaveBeenCalled ( ) ;
1611+ } ) ;
1612+
15241613 it ( "probes the channel primary when a session is pinned to an auto fallback" , async ( ) => {
15251614 setupSingleAttemptFallback ( ) ;
15261615 state . resolveAutoFallbackPrimaryProbeMock . mockReturnValue ( {
0 commit comments