@@ -1429,4 +1429,72 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => {
14291429 } ) . pipe ( Effect . provide ( customAdapterLayer ) ) ;
14301430 } ,
14311431 ) ;
1432+
1433+ // Production calls startSession from a request fiber that finishes as soon as
1434+ // the session exists. `Effect.forkChild` made the notification consumer a
1435+ // child of that fiber, and Effect interrupts a fiber's children when it
1436+ // completes, so the consumer died on return and every later session/update
1437+ // was dropped: the thread sat on "Working" forever while the provider
1438+ // streamed its whole turn. The other tests here call startSession directly
1439+ // from the test fiber, which never completes, so the consumer survived and
1440+ // the bug stayed invisible. Running it in a fiber that finishes is what
1441+ // reproduces production.
1442+ it . effect ( "keeps consuming notifications after the startSession fiber completes" , ( ) =>
1443+ Effect . gen ( function * ( ) {
1444+ const adapter = yield * CursorAdapter ;
1445+ const settings = yield * ServerSettingsService ;
1446+ const threadId = ThreadId . make ( "cursor-consumer-outlives-start-session" ) ;
1447+
1448+ const wrapperPath = yield * Effect . promise ( ( ) => makeMockAgentWrapper ( ) ) ;
1449+ yield * settings . updateSettings ( { providers : { cursor : { binaryPath : wrapperPath } } } ) ;
1450+
1451+ const runtimeEvents : ProviderRuntimeEvent [ ] = [ ] ;
1452+ const sawContentDelta = yield * Deferred . make < void > ( ) ;
1453+ const runtimeEventsFiber = yield * Stream . runForEach ( adapter . streamEvents , ( event ) =>
1454+ Effect . sync ( ( ) => {
1455+ runtimeEvents . push ( event ) ;
1456+ } ) . pipe (
1457+ Effect . andThen (
1458+ event . type === "content.delta" && String ( event . threadId ) === String ( threadId )
1459+ ? Deferred . succeed ( sawContentDelta , undefined ) . pipe ( Effect . asVoid )
1460+ : Effect . void ,
1461+ ) ,
1462+ ) ,
1463+ ) . pipe ( Effect . forkChild ) ;
1464+
1465+ const startSessionFiber = yield * adapter
1466+ . startSession ( {
1467+ threadId,
1468+ provider : ProviderDriverKind . make ( "cursor" ) ,
1469+ cwd : process . cwd ( ) ,
1470+ runtimeMode : "full-access" ,
1471+ modelSelection : { instanceId : ProviderInstanceId . make ( "cursor" ) , model : "default" } ,
1472+ } )
1473+ . pipe ( Effect . forkChild ) ;
1474+ yield * Fiber . join ( startSessionFiber ) . pipe ( Effect . timeout ( "10 seconds" ) ) ;
1475+
1476+ // Forked, and the assertion waits on the projected event rather than on
1477+ // sendTurn: with the consumer dead the turn never settles, so awaiting it
1478+ // directly would hang until the suite timeout instead of failing here.
1479+ const sendTurnFiber = yield * adapter
1480+ . sendTurn ( { threadId, input : "hello mock" , attachments : [ ] } )
1481+ . pipe ( Effect . forkChild ) ;
1482+ yield * Deferred . await ( sawContentDelta ) . pipe ( Effect . timeout ( "10 seconds" ) ) ;
1483+ yield * Fiber . join ( sendTurnFiber ) . pipe ( Effect . timeout ( "10 seconds" ) ) ;
1484+
1485+ const delta = runtimeEvents . find (
1486+ ( event ) => event . type === "content.delta" && String ( event . threadId ) === String ( threadId ) ,
1487+ ) ;
1488+ assert . isDefined (
1489+ delta ,
1490+ "no content.delta was projected after the startSession fiber completed" ,
1491+ ) ;
1492+
1493+ yield * Fiber . interrupt ( runtimeEventsFiber ) ;
1494+ yield * adapter . stopSession ( threadId ) ;
1495+ // Live clock so the timeouts above are real: under the default test clock
1496+ // they wait on virtual time that never advances, and a regression would
1497+ // hang until the suite timeout instead of failing here.
1498+ } ) . pipe ( TestClock . withLive ) ,
1499+ ) ;
14321500} ) ;
0 commit comments