@@ -1376,6 +1376,168 @@ process.on("SIGINT", shutdown);`,
13761376 expect ( manager . listSessionIds ( ) ) . not . toContain ( "session-a" ) ;
13771377 } ) ;
13781378
1379+ it ( "keeps separate runtimes for different sessions by default" , async ( ) => {
1380+ const created : string [ ] = [ ] ;
1381+ const createRuntime : RuntimeFactory = ( params ) => {
1382+ created . push ( params . sessionId ) ;
1383+ return {
1384+ ...makeRuntime ( [ { toolName : "bundle_probe" , description : "Bundle MCP probe" } ] ) ,
1385+ sessionId : params . sessionId ,
1386+ sessionKey : params . sessionKey ,
1387+ workspaceDir : params . workspaceDir ,
1388+ configFingerprint : params . configFingerprint ?? "fingerprint" ,
1389+ } ;
1390+ } ;
1391+ const manager = testing . createSessionMcpRuntimeManager ( { createRuntime } ) ;
1392+
1393+ const runtimeA = await manager . getOrCreate ( {
1394+ sessionId : "session-a" ,
1395+ sessionKey : "agent:test:session-a" ,
1396+ workspaceDir : "/workspace" ,
1397+ cfg : { mcp : { servers : { } } } ,
1398+ } ) ;
1399+ const runtimeB = await manager . getOrCreate ( {
1400+ sessionId : "session-b" ,
1401+ sessionKey : "agent:test:session-b" ,
1402+ workspaceDir : "/workspace" ,
1403+ cfg : { mcp : { servers : { } } } ,
1404+ } ) ;
1405+
1406+ expect ( runtimeA ) . not . toBe ( runtimeB ) ;
1407+ expect ( created ) . toEqual ( [ "session-a" , "session-b" ] ) ;
1408+ expect ( manager . listSessionIds ( ) . toSorted ( ) ) . toEqual ( [ "session-a" , "session-b" ] ) ;
1409+ } ) ;
1410+
1411+ it ( "shares one runtime across sessions when runtimeScope is shared" , async ( ) => {
1412+ const created : string [ ] = [ ] ;
1413+ const disposed : string [ ] = [ ] ;
1414+ const createRuntime : RuntimeFactory = ( params ) => {
1415+ created . push ( params . sessionId ) ;
1416+ return {
1417+ ...makeRuntime ( [ { toolName : "bundle_probe" , description : "Bundle MCP probe" } ] ) ,
1418+ sessionId : params . sessionId ,
1419+ sessionKey : params . sessionKey ,
1420+ workspaceDir : params . workspaceDir ,
1421+ configFingerprint : params . configFingerprint ?? "fingerprint" ,
1422+ dispose : async ( ) => {
1423+ disposed . push ( params . sessionId ) ;
1424+ } ,
1425+ } ;
1426+ } ;
1427+ const manager = testing . createSessionMcpRuntimeManager ( { createRuntime } ) ;
1428+ const cfg = { mcp : { runtimeScope : "shared" as const , servers : { } } } ;
1429+
1430+ const runtimeA = await manager . getOrCreate ( {
1431+ sessionId : "session-a" ,
1432+ sessionKey : "agent:test:session-a" ,
1433+ workspaceDir : "/workspace" ,
1434+ cfg,
1435+ } ) ;
1436+ const runtimeB = await manager . getOrCreate ( {
1437+ sessionId : "session-b" ,
1438+ sessionKey : "agent:test:session-b" ,
1439+ workspaceDir : "/workspace" ,
1440+ cfg,
1441+ } ) ;
1442+
1443+ expect ( runtimeA ) . toBe ( runtimeB ) ;
1444+ expect ( created ) . toHaveLength ( 1 ) ;
1445+ expect ( created [ 0 ] ) . toMatch ( / ^ _ _ m c p _ s h a r e d _ _ : / ) ;
1446+ expect ( manager . peekSession ( { sessionId : "session-a" } ) ) . toBe ( runtimeA ) ;
1447+ expect ( manager . peekSession ( { sessionKey : "agent:test:session-b" } ) ) . toBe ( runtimeA ) ;
1448+ expect ( manager . listSessionIds ( ) . toSorted ( ) ) . toEqual ( [ "session-a" , "session-b" ] ) ;
1449+
1450+ await manager . disposeSession ( "session-a" ) ;
1451+
1452+ expect ( disposed ) . toEqual ( [ ] ) ;
1453+ expect ( manager . peekSession ( { sessionId : "session-a" } ) ) . toBeUndefined ( ) ;
1454+ expect ( manager . peekSession ( { sessionId : "session-b" } ) ) . toBe ( runtimeA ) ;
1455+
1456+ await manager . disposeSession ( "session-b" ) ;
1457+
1458+ expect ( disposed ) . toEqual ( [ runtimeA . sessionId ] ) ;
1459+ expect ( manager . listSessionIds ( ) ) . toEqual ( [ ] ) ;
1460+ } ) ;
1461+
1462+ it ( "creates separate shared runtimes per workspace and MCP config fingerprint" , async ( ) => {
1463+ const created : Array < { sessionId : string ; workspaceDir : string ; configFingerprint ?: string } > =
1464+ [ ] ;
1465+ const createRuntime : RuntimeFactory = ( params ) => {
1466+ created . push ( {
1467+ sessionId : params . sessionId ,
1468+ workspaceDir : params . workspaceDir ,
1469+ configFingerprint : params . configFingerprint ,
1470+ } ) ;
1471+ return {
1472+ ...makeRuntime ( [ { toolName : "bundle_probe" , description : "Bundle MCP probe" } ] ) ,
1473+ sessionId : params . sessionId ,
1474+ workspaceDir : params . workspaceDir ,
1475+ configFingerprint : params . configFingerprint ?? "fingerprint" ,
1476+ } ;
1477+ } ;
1478+ const manager = testing . createSessionMcpRuntimeManager ( { createRuntime } ) ;
1479+ const cfgA = {
1480+ mcp : {
1481+ runtimeScope : "shared" as const ,
1482+ servers : { probe : { command : "node" , args : [ "server-a.mjs" ] } } ,
1483+ } ,
1484+ } ;
1485+ const cfgB = {
1486+ mcp : {
1487+ runtimeScope : "shared" as const ,
1488+ servers : { probe : { command : "node" , args : [ "server-b.mjs" ] } } ,
1489+ } ,
1490+ } ;
1491+
1492+ const runtimeA = await manager . getOrCreate ( {
1493+ sessionId : "session-a" ,
1494+ workspaceDir : "/workspace-a" ,
1495+ cfg : cfgA ,
1496+ } ) ;
1497+ const runtimeSameFingerprint = await manager . getOrCreate ( {
1498+ sessionId : "session-b" ,
1499+ workspaceDir : "/workspace-a" ,
1500+ cfg : cfgA ,
1501+ } ) ;
1502+ const runtimeDifferentWorkspace = await manager . getOrCreate ( {
1503+ sessionId : "session-c" ,
1504+ workspaceDir : "/workspace-b" ,
1505+ cfg : cfgA ,
1506+ } ) ;
1507+ const runtimeDifferentConfig = await manager . getOrCreate ( {
1508+ sessionId : "session-d" ,
1509+ workspaceDir : "/workspace-a" ,
1510+ cfg : cfgB ,
1511+ } ) ;
1512+
1513+ expect ( runtimeSameFingerprint ) . toBe ( runtimeA ) ;
1514+ expect ( runtimeDifferentWorkspace ) . not . toBe ( runtimeA ) ;
1515+ expect ( runtimeDifferentConfig ) . not . toBe ( runtimeA ) ;
1516+ expect ( created ) . toHaveLength ( 3 ) ;
1517+ expect ( new Set ( created . map ( ( entry ) => entry . sessionId ) ) . size ) . toBe ( 3 ) ;
1518+ } ) ;
1519+
1520+ it ( "drops session attachments when shared runtime creation fails" , async ( ) => {
1521+ const manager = testing . createSessionMcpRuntimeManager ( {
1522+ createRuntime : ( ) => {
1523+ throw new Error ( "mcp child failed to start" ) ;
1524+ } ,
1525+ } ) ;
1526+
1527+ await expect (
1528+ manager . getOrCreate ( {
1529+ sessionId : "session-a" ,
1530+ sessionKey : "agent:test:session-a" ,
1531+ workspaceDir : "/workspace" ,
1532+ cfg : { mcp : { runtimeScope : "shared" , servers : { } } } ,
1533+ } ) ,
1534+ ) . rejects . toThrow ( "mcp child failed to start" ) ;
1535+
1536+ expect ( manager . listSessionIds ( ) ) . toEqual ( [ ] ) ;
1537+ expect ( manager . peekSession ( { sessionId : "session-a" } ) ) . toBeUndefined ( ) ;
1538+ expect ( manager . resolveSessionId ( "agent:test:session-a" ) ) . toBeUndefined ( ) ;
1539+ } ) ;
1540+
13791541 it ( "peeks existing runtimes and populated catalogs without creating new runtimes" , async ( ) => {
13801542 let catalogReady = false ;
13811543 const createRuntime : RuntimeFactory = ( params ) => {
@@ -1730,6 +1892,73 @@ process.on("SIGINT", shutdown);`,
17301892 expect ( disposed ) . toStrictEqual ( [ ] ) ;
17311893 } ) ;
17321894
1895+ it ( "does not evict a shared runtime while an attached session holds a lease" , async ( ) => {
1896+ let now = 1_000 ;
1897+ const disposed : string [ ] = [ ] ;
1898+ const createRuntime : RuntimeFactory = ( params ) => {
1899+ let lastUsedAt = now ;
1900+ let activeLeases = 0 ;
1901+ return {
1902+ ...makeRuntime ( [ { toolName : "bundle_probe" , description : "Bundle MCP probe" } ] ) ,
1903+ sessionId : params . sessionId ,
1904+ workspaceDir : params . workspaceDir ,
1905+ configFingerprint : params . configFingerprint ?? "fingerprint" ,
1906+ get lastUsedAt ( ) {
1907+ return lastUsedAt ;
1908+ } ,
1909+ get activeLeases ( ) {
1910+ return activeLeases ;
1911+ } ,
1912+ markUsed : ( ) => {
1913+ lastUsedAt = now ;
1914+ } ,
1915+ acquireLease : ( ) => {
1916+ activeLeases += 1 ;
1917+ return ( ) => {
1918+ activeLeases -= 1 ;
1919+ } ;
1920+ } ,
1921+ dispose : async ( ) => {
1922+ disposed . push ( params . sessionId ) ;
1923+ } ,
1924+ } ;
1925+ } ;
1926+ const manager = testing . createSessionMcpRuntimeManager ( {
1927+ createRuntime,
1928+ now : ( ) => now ,
1929+ enableIdleSweepTimer : false ,
1930+ } ) ;
1931+ const cfg = {
1932+ mcp : {
1933+ runtimeScope : "shared" as const ,
1934+ servers : { } ,
1935+ sessionIdleTtlMs : 50 ,
1936+ } ,
1937+ } ;
1938+
1939+ const runtime = await manager . getOrCreate ( {
1940+ sessionId : "session-a" ,
1941+ workspaceDir : "/workspace" ,
1942+ cfg,
1943+ } ) ;
1944+ await manager . getOrCreate ( {
1945+ sessionId : "session-b" ,
1946+ workspaceDir : "/workspace" ,
1947+ cfg,
1948+ } ) ;
1949+ const releaseLease = runtime . acquireLease ?.( ) ;
1950+
1951+ now += 60 ;
1952+ await expect ( manager . sweepIdleRuntimes ( ) ) . resolves . toBe ( 0 ) ;
1953+ expect ( manager . listSessionIds ( ) . toSorted ( ) ) . toEqual ( [ "session-a" , "session-b" ] ) ;
1954+
1955+ releaseLease ?.( ) ;
1956+ await expect ( manager . sweepIdleRuntimes ( ) ) . resolves . toBe ( 1 ) ;
1957+
1958+ expect ( disposed ) . toEqual ( [ runtime . sessionId ] ) ;
1959+ expect ( manager . listSessionIds ( ) ) . toEqual ( [ ] ) ;
1960+ } ) ;
1961+
17331962 it ( "production createSessionMcpRuntime acquireLease release does not refresh lastUsedAt" , ( ) => {
17341963 const runtime = createSessionMcpRuntime ( {
17351964 sessionId : "session-lease-timestamp-check" ,
0 commit comments