@@ -99,9 +99,77 @@ function installScopedSessionStores(syncUpdates = false) {
9999async function createSessionsModuleMock ( ) {
100100 const actual =
101101 await vi . importActual < typeof import ( "../config/sessions.js" ) > ( "../config/sessions.js" ) ;
102+ const resolveMockStorePath = ( _store : string | undefined , opts ?: { agentId ?: string } ) =>
103+ opts ?. agentId === "support" ? "/tmp/support/sessions.json" : "/tmp/main/sessions.json" ;
104+ const cloneEntry = ( entry : SessionEntry ) : SessionEntry => structuredClone ( entry ) ;
102105 return {
103106 ...actual ,
104107 loadSessionStore : ( storePath : string ) => loadSessionStoreMock ( storePath ) ,
108+ patchSessionEntryWithKey : async (
109+ scope : { agentId ?: string ; sessionKey : string ; storePath ?: string } ,
110+ update : (
111+ entry : SessionEntry ,
112+ context : { existingEntry ?: SessionEntry } ,
113+ ) => Promise < Partial < SessionEntry > | null > | Partial < SessionEntry > | null ,
114+ options ?: { fallbackEntry ?: SessionEntry ; replaceEntry ?: boolean } ,
115+ ) => {
116+ const storePath =
117+ scope . storePath ?? resolveMockStorePath ( undefined , { agentId : scope . agentId } ) ;
118+ const store = loadSessionStoreMock ( storePath ) as Record < string , SessionEntry > ;
119+ const resolved = actual . resolveSessionStoreEntry ( { store, sessionKey : scope . sessionKey } ) ;
120+ const existing = resolved . existing ?? options ?. fallbackEntry ;
121+ if ( ! existing ) {
122+ return null ;
123+ }
124+ const patch = await update ( cloneEntry ( existing ) , {
125+ existingEntry : resolved . existing ? cloneEntry ( resolved . existing ) : undefined ,
126+ } ) ;
127+ if ( ! patch ) {
128+ return { sessionKey : resolved . normalizedKey , entry : cloneEntry ( existing ) } ;
129+ }
130+ const next = options ?. replaceEntry
131+ ? cloneEntry ( patch as SessionEntry )
132+ : actual . mergeSessionEntry ( existing , patch ) ;
133+ store [ resolved . normalizedKey ] = next ;
134+ updateSessionStoreMock ( storePath , store ) ;
135+ return { sessionKey : resolved . normalizedKey , entry : cloneEntry ( next ) } ;
136+ } ,
137+ resolveSessionEntryCandidateTarget : ( scope : {
138+ agentId : string ;
139+ candidateKeys : readonly string [ ] ;
140+ cfg : { session ?: { store ?: string } } ;
141+ fallback ?: { sessionKey : string ; entry : SessionEntry } ;
142+ } ) => {
143+ const storePath = resolveMockStorePath ( scope . cfg . session ?. store , { agentId : scope . agentId } ) ;
144+ const store = loadSessionStoreMock ( storePath ) as Record < string , SessionEntry > ;
145+ const candidates = [ ...new Set ( scope . candidateKeys . map ( ( key ) => key . trim ( ) ) ) ] ;
146+ for ( const candidateKey of candidates ) {
147+ if ( ! candidateKey ) {
148+ continue ;
149+ }
150+ const resolved = actual . resolveSessionStoreEntry ( { store, sessionKey : candidateKey } ) ;
151+ if ( ! resolved . existing ) {
152+ continue ;
153+ }
154+ return {
155+ agentId : scope . agentId ,
156+ candidateKey,
157+ entry : cloneEntry ( resolved . existing ) ,
158+ persisted : true ,
159+ sessionKey : resolved . normalizedKey ,
160+ } ;
161+ }
162+ const fallbackKey = scope . fallback ?. sessionKey . trim ( ) ;
163+ return fallbackKey && scope . fallback
164+ ? {
165+ agentId : scope . agentId ,
166+ candidateKey : fallbackKey ,
167+ entry : cloneEntry ( scope . fallback . entry ) ,
168+ persisted : false ,
169+ sessionKey : fallbackKey ,
170+ }
171+ : null ;
172+ } ,
105173 updateSessionStore : async (
106174 storePath : string ,
107175 mutator : ( store : Record < string , unknown > ) => Promise < void > | void ,
@@ -111,8 +179,7 @@ async function createSessionsModuleMock() {
111179 updateSessionStoreMock ( storePath , store ) ;
112180 return store ;
113181 } ,
114- resolveStorePath : ( _store : string | undefined , opts ?: { agentId ?: string } ) =>
115- opts ?. agentId === "support" ? "/tmp/support/sessions.json" : "/tmp/main/sessions.json" ,
182+ resolveStorePath : resolveMockStorePath ,
116183 } ;
117184}
118185
@@ -1191,6 +1258,41 @@ describe("session_status tool", () => {
11911258 expect ( saved . sessionId ) . toMatch ( UUID_RE ) ;
11921259 } ) ;
11931260
1261+ it ( "preserves an existing legacy main row when implicit fallback mutates model state" , async ( ) => {
1262+ resetSessionStore ( {
1263+ main : {
1264+ sessionId : "legacy-main-session" ,
1265+ updatedAt : 10 ,
1266+ label : "Legacy Main" ,
1267+ lastChannel : "telegram" ,
1268+ } ,
1269+ } ) ;
1270+
1271+ const tool = getSessionStatusTool ( "agent:main:main" ) ;
1272+
1273+ const result = await tool . execute ( "call-legacy-main-fallback-model" , {
1274+ model : "anthropic/claude-sonnet-4-6" ,
1275+ } ) ;
1276+ const details = result . details as {
1277+ ok ?: boolean ;
1278+ sessionKey ?: string ;
1279+ modelOverride ?: string | null ;
1280+ } ;
1281+ expect ( details . ok ) . toBe ( true ) ;
1282+ expect ( details . sessionKey ) . toBe ( "main" ) ;
1283+ expect ( details . modelOverride ) . toBe ( "anthropic/claude-sonnet-4-6" ) ;
1284+ expect ( updateSessionStoreMock ) . toHaveBeenCalledTimes ( 1 ) ;
1285+ const savedStore = latestMockCallArg ( updateSessionStoreMock , 1 ) as Record < string , SessionEntry > ;
1286+ expect ( savedStore . main ) . toMatchObject ( {
1287+ sessionId : "legacy-main-session" ,
1288+ label : "Legacy Main" ,
1289+ lastChannel : "telegram" ,
1290+ providerOverride : "anthropic" ,
1291+ modelOverride : "claude-sonnet-4-6" ,
1292+ liveModelSwitchPending : true ,
1293+ } ) ;
1294+ } ) ;
1295+
11941296 it ( "fires session:patch when session_status changes the persisted session model" , async ( ) => {
11951297 const events : InternalHookEvent [ ] = [ ] ;
11961298 registerInternalHook ( "session:patch" , async ( event ) => {
0 commit comments