@@ -64,9 +64,14 @@ export type BackgroundShellSnapshot =
6464
6565export interface BackgroundShellRegistry {
6666 start : ( args : StartBackgroundShellArgs ) => { id : string } | { error : string } ;
67- collect : ( id : string , waitMs ?: number ) => Promise < BackgroundShellSnapshot > ;
67+ collect : (
68+ id : string ,
69+ waitMs ?: number ,
70+ signal ?: AbortSignal ,
71+ ) => Promise < BackgroundShellSnapshot > ;
6872 cancel : ( id : string ) => boolean ;
6973 disposeAll : ( reason : string ) => void ;
74+ releaseWaiters : ( ) => void ;
7075 runningCount : ( ) => number ;
7176}
7277
@@ -151,14 +156,33 @@ export function createBackgroundShellRegistry(
151156 const collect = async (
152157 id : string ,
153158 waitMs = 0 ,
159+ signal ?: AbortSignal ,
154160 ) : Promise < BackgroundShellSnapshot > => {
155161 const done = completed . get ( id ) ;
156162 if ( done !== undefined ) return { state : "completed" , exit : done } ;
157163 if ( ! running . has ( id ) ) return { state : "not-found" } ;
158164 if ( waitMs > 0 ) {
165+ // An already-aborted collect releases immediately as still-running:
166+ // interrupt must not park the session on a live descendant, and must
167+ // not kill it either — the child belongs to the still-alive session.
168+ if ( signal ?. aborted === true ) return { state : "running" } ;
159169 await new Promise < void > ( ( resolve ) => {
160- exitWaiters . set ( id , resolve ) ;
161- setTimeout ( resolve , waitMs ) ;
170+ const timer = setTimeout ( ( ) => {
171+ exitWaiters . delete ( id ) ;
172+ signal ?. removeEventListener ( "abort" , onAbort ) ;
173+ resolve ( ) ;
174+ } , waitMs ) ;
175+ const onAbort = ( ) : void => {
176+ clearTimeout ( timer ) ;
177+ exitWaiters . delete ( id ) ;
178+ resolve ( ) ;
179+ } ;
180+ signal ?. addEventListener ( "abort" , onAbort , { once : true } ) ;
181+ exitWaiters . set ( id , ( ) => {
182+ clearTimeout ( timer ) ;
183+ signal ?. removeEventListener ( "abort" , onAbort ) ;
184+ resolve ( ) ;
185+ } ) ;
162186 } ) . finally ( ( ) => exitWaiters . delete ( id ) ) ;
163187 const finished = completed . get ( id ) ;
164188 if ( finished !== undefined ) return { state : "completed" , exit : finished } ;
@@ -173,12 +197,22 @@ export function createBackgroundShellRegistry(
173197 return true ;
174198 } ;
175199
200+ /**
201+ * Wake every parked `collect` waiter as still-running without touching the
202+ * children. Interrupt paths call this so a live descendant releases the
203+ * session instead of wedging teardown; close paths use `disposeAll`, which
204+ * wakes waiters and then kills the trees.
205+ */
206+ const releaseWaiters = ( ) : void => {
207+ for ( const wake of exitWaiters . values ( ) ) wake ( ) ;
208+ exitWaiters . clear ( ) ;
209+ } ;
210+
176211 const disposeAll = ( reason : string ) : void => {
177212 for ( const child of running . values ( ) ) killProcessTree ( child ) ;
178213 running . clear ( ) ;
179214 completed . clear ( ) ;
180- for ( const wake of exitWaiters . values ( ) ) wake ( ) ;
181- exitWaiters . clear ( ) ;
215+ releaseWaiters ( ) ;
182216 // onExit is intentionally not fired for disposed shells: the session is
183217 // gone, so there is no later turn to deliver to (`reason` is for callers
184218 // that log it).
@@ -190,6 +224,7 @@ export function createBackgroundShellRegistry(
190224 collect,
191225 cancel,
192226 disposeAll,
227+ releaseWaiters,
193228 runningCount : ( ) => running . size ,
194229 } ;
195230}
0 commit comments