@@ -77,6 +77,7 @@ import {
7777 promptReferencesAbsoluteFolder ,
7878 shellExecutesToContextBlocks ,
7979} from "./sessionEvents" ;
80+ import { selectSessionsToEvict } from "./sessionEviction" ;
8081import { createBaseSession } from "./sessionFactory" ;
8182import { type ParsedSessionLogs , parseSessionLogContent } from "./sessionLogs" ;
8283
@@ -513,6 +514,8 @@ export class SessionService {
513514 > ( ) ;
514515 private localRepoPaths = new Map < string , string > ( ) ;
515516 private localRecoveryAttempts = new Map < string , Promise < boolean > > ( ) ;
517+ private sessionLastUsedAt = new Map < string , number > ( ) ;
518+ private mountedTaskCounts = new Map < string , number > ( ) ;
516519 /** Re-entrance guard for cloud queue dispatch (per taskId). */
517520 private dispatchingCloudQueues = new Set < string > ( ) ;
518521 /** Coalesces deferred cloud queue flush timers (per taskId). */
@@ -610,6 +613,8 @@ export class SessionService {
610613 const { task } = params ;
611614 const taskId = task . id ;
612615 this . localRepoPaths . set ( taskId , params . repoPath ) ;
616+ this . sessionLastUsedAt . set ( taskId , Date . now ( ) ) ;
617+ void this . evictIdleSessions ( taskId ) ;
613618
614619 // Return existing connection promise if already connecting
615620 const existingPromise = this . connectingTasks . get ( taskId ) ;
@@ -1029,7 +1034,10 @@ export class SessionService {
10291034 }
10301035 }
10311036
1032- private async teardownSession ( taskRunId : string ) : Promise < void > {
1037+ private async teardownSession (
1038+ taskRunId : string ,
1039+ opts ?: { preserveResumeState ?: boolean } ,
1040+ ) : Promise < void > {
10331041 const session = this . getSessionByRunId ( taskRunId ) ;
10341042
10351043 try {
@@ -1053,9 +1061,14 @@ export class SessionService {
10531061 if ( session ) {
10541062 this . localRepoPaths . delete ( session . taskId ) ;
10551063 this . localRecoveryAttempts . delete ( session . taskId ) ;
1064+ this . sessionLastUsedAt . delete ( session . taskId ) ;
1065+ }
1066+ if ( ! opts ?. preserveResumeState ) {
1067+ // Reconnect restores the model and permission mode from these; only a
1068+ // permanent disconnect (archive, delete, fresh session) may drop them.
1069+ this . d . adapterStore . removeAdapter ( taskRunId ) ;
1070+ this . d . removePersistedConfigOptions ( taskRunId ) ;
10561071 }
1057- this . d . adapterStore . removeAdapter ( taskRunId ) ;
1058- this . d . removePersistedConfigOptions ( taskRunId ) ;
10591072 }
10601073
10611074 /**
@@ -1358,6 +1371,51 @@ export class SessionService {
13581371 await this . teardownSession ( session . taskRunId ) ;
13591372 }
13601373
1374+ registerMountedTask ( taskId : string ) : ( ) => void {
1375+ this . mountedTaskCounts . set (
1376+ taskId ,
1377+ ( this . mountedTaskCounts . get ( taskId ) ?? 0 ) + 1 ,
1378+ ) ;
1379+ this . sessionLastUsedAt . set ( taskId , Date . now ( ) ) ;
1380+ return ( ) => {
1381+ const count = this . mountedTaskCounts . get ( taskId ) ?? 0 ;
1382+ if ( count <= 1 ) {
1383+ this . mountedTaskCounts . delete ( taskId ) ;
1384+ } else {
1385+ this . mountedTaskCounts . set ( taskId , count - 1 ) ;
1386+ }
1387+ this . sessionLastUsedAt . set ( taskId , Date . now ( ) ) ;
1388+ } ;
1389+ }
1390+
1391+ private async evictIdleSessions ( activeTaskId : string ) : Promise < void > {
1392+ const toEvict = selectSessionsToEvict ( {
1393+ sessions : Object . values ( this . d . store . getSessions ( ) ) ,
1394+ activeTaskId,
1395+ protectedTaskIds : new Set ( this . mountedTaskCounts . keys ( ) ) ,
1396+ lastUsedAt : ( session ) =>
1397+ this . sessionLastUsedAt . get ( session . taskId ) ?? session . startedAt ,
1398+ } ) ;
1399+
1400+ for ( const session of toEvict ) {
1401+ this . d . log . info ( "Evicting idle session to bound memory" , {
1402+ taskId : session . taskId ,
1403+ taskRunId : session . taskRunId ,
1404+ } ) ;
1405+ this . sessionLastUsedAt . delete ( session . taskId ) ;
1406+ try {
1407+ await this . teardownSession ( session . taskRunId , {
1408+ preserveResumeState : true ,
1409+ } ) ;
1410+ } catch ( error ) {
1411+ this . d . log . error ( "Failed to evict idle session" , {
1412+ taskId : session . taskId ,
1413+ error,
1414+ } ) ;
1415+ }
1416+ }
1417+ }
1418+
13611419 // --- Subscription Management ---
13621420
13631421 /** Streamed events awaiting their frame flush, keyed by taskRunId. Order
@@ -1598,6 +1656,7 @@ export class SessionService {
15981656 this . connectingTasks . clear ( ) ;
15991657 this . localRepoPaths . clear ( ) ;
16001658 this . localRecoveryAttempts . clear ( ) ;
1659+ this . sessionLastUsedAt . clear ( ) ;
16011660 this . cloudPermissionRequestIds . clear ( ) ;
16021661 this . liveTurnContent . clear ( ) ;
16031662 this . cloudLogGapReconciler . clear ( ) ;
@@ -4285,6 +4344,10 @@ export class SessionService {
42854344 } = params ;
42864345
42874346 if ( isCloud ) {
4347+ // Local connects bound the session budget inside connectToTask; cloud
4348+ // watches would otherwise never trigger eviction.
4349+ this . sessionLastUsedAt . set ( task . id , Date . now ( ) ) ;
4350+ void this . evictIdleSessions ( task . id ) ;
42884351 return this . reconcileCloudConnection (
42894352 task ,
42904353 cloudAuth ,
0 commit comments