@@ -51,7 +51,7 @@ import {
5151} from '../storage/sessionPaths.js' ;
5252import {
5353 addAbortListener ,
54- makeAbortError ,
54+ makeAbortReason ,
5555 throwIfAborted ,
5656 waitForScopedOperation ,
5757} from '../util/abort.js' ;
@@ -177,7 +177,6 @@ export async function runHost(sessionId: string): Promise<void> {
177177 let lastOutputAt = Date . now ( ) ;
178178 let lastActivityAt = lastOutputAt ;
179179 const hostAbortController = new AbortController ( ) ;
180- let idleTimeoutHandle : ReturnType < typeof setInterval > | null = null ;
181180 let idleTimeoutScope : ResourceScope | null = null ;
182181 let rpcListenPromise : Promise < void > | null = null ;
183182 let shutdownPromise : Promise < void > | null = null ;
@@ -193,6 +192,10 @@ export async function runHost(sessionId: string): Promise<void> {
193192 } ) ;
194193 let ptyIngestionQueue : Promise < void > = Promise . resolve ( ) ;
195194
195+ // Per-client wait-exit callbacks, cleaned up individually via ResourceScope.
196+ // Using ptyExitPromise.then() would permanently attach to the shared promise.
197+ const ptyExitWaiters = new Set < ( ) => void > ( ) ;
198+
196199 const ptyExitPromise = new Promise < void > ( ( resolve ) => {
197200 markPtyExited = ( ) : void => {
198201 if ( ptyHasExited ) {
@@ -201,6 +204,11 @@ export async function runHost(sessionId: string): Promise<void> {
201204
202205 ptyHasExited = true ;
203206 resolve ( ) ;
207+ const waiters = [ ...ptyExitWaiters ] ;
208+ ptyExitWaiters . clear ( ) ;
209+ for ( const waiter of waiters ) {
210+ waiter ( ) ;
211+ }
204212 } ;
205213 } ) ;
206214
@@ -282,6 +290,7 @@ export async function runHost(sessionId: string): Promise<void> {
282290 const scope = new ResourceScope ( ) ;
283291 idleTimeoutScope = scope ;
284292 const checkIntervalMs = Math . min ( idleTimeoutMs , IDLE_CHECK_CAP_MS ) ;
293+ let idleTimeoutHandle : ReturnType < typeof setInterval > | null = null ;
285294 idleTimeoutHandle = setInterval ( ( ) => {
286295 if ( hostAbortController . signal . aborted ) {
287296 clearIdleTimeout ( ) ;
@@ -322,7 +331,7 @@ export async function runHost(sessionId: string): Promise<void> {
322331
323332 shutdownPromise = ( async ( ) => {
324333 try {
325- hostAbortController . abort ( makeAbortError ( ) ) ;
334+ hostAbortController . abort ( makeAbortReason ( 'Host is shutting down.' ) ) ;
326335 clearIdleTimeout ( ) ;
327336 if ( isSessionCommandable ( state ) ) {
328337 pty . kill ( ) ;
@@ -391,6 +400,15 @@ export async function runHost(sessionId: string): Promise<void> {
391400 } ) ( ) . catch ( rethrowAsync ) ;
392401 } ;
393402
403+ const makeWaitExitOutcome = ( ) : WaitOutcome => {
404+ const snapshot = state . snapshot ( ) ;
405+ const result : WaitOutcome = { timedOut : false } ;
406+ if ( snapshot . exitCode !== null ) {
407+ result . exitCode = snapshot . exitCode ;
408+ }
409+ return result ;
410+ } ;
411+
394412 const handlers : Record < string , MethodHandler > = {
395413 inspect : ( ) => Promise . resolve ( { session : state . snapshot ( ) } ) ,
396414 snapshot : async ( params : unknown ) => {
@@ -723,21 +741,17 @@ export async function runHost(sessionId: string): Promise<void> {
723741
724742 if ( hasExit ) {
725743 if ( ptyHasExited ) {
726- const snapshot = state . snapshot ( ) ;
727- const result : WaitOutcome = { timedOut : false } ;
728- if ( snapshot . exitCode !== null ) {
729- result . exitCode = snapshot . exitCode ;
730- }
731- return result ;
744+ return makeWaitExitOutcome ( ) ;
732745 }
733746
734- waitCondition = ptyExitPromise . then ( ( ) => {
735- const snapshot = state . snapshot ( ) ;
736- const result : WaitOutcome = { timedOut : false } ;
737- if ( snapshot . exitCode !== null ) {
738- result . exitCode = snapshot . exitCode ;
739- }
740- return result ;
747+ waitCondition = new Promise < WaitOutcome > ( ( resolve ) => {
748+ const waiter = ( ) : void => {
749+ resolve ( makeWaitExitOutcome ( ) ) ;
750+ } ;
751+ ptyExitWaiters . add ( waiter ) ;
752+ waitScope . add ( 'wait exit waiter' , ( ) => {
753+ ptyExitWaiters . delete ( waiter ) ;
754+ } ) ;
741755 } ) ;
742756 } else {
743757 assertSessionCommandable ( state ) ;
0 commit comments