@@ -22,6 +22,26 @@ import type { AdmissionQueue, AdmissionStatus } from "./admission.js";
2222
2323const log = getLogger ( [ LOG_NAMESPACE_ROOT , "subagent" , "session-store" ] ) ;
2424
25+ async function invokeCloseBounded (
26+ close : ( deadlineMs ?: number ) => Promise < void > ,
27+ deadlineMs : number ,
28+ ) : Promise < void > {
29+ let closeError : unknown ;
30+ await Promise . race ( [
31+ close ( deadlineMs ) . then (
32+ ( ) => undefined ,
33+ ( err : unknown ) => {
34+ closeError = err ;
35+ log . warn ( "session close raced deadline: {error}" , {
36+ error : err instanceof Error ? err . message : String ( err ) ,
37+ } ) ;
38+ } ,
39+ ) ,
40+ new Promise < void > ( ( resolve ) => setTimeout ( resolve , deadlineMs ) ) ,
41+ ] ) ;
42+ if ( closeError !== undefined ) throw closeError ;
43+ }
44+
2545export type SubAgentSessionStatus = "running" | "done" | "failed" | "cancelled" ;
2646
2747/**
@@ -194,8 +214,10 @@ export interface SubAgentSessionStore {
194214 // Abort a running session and mark it cancelled. Returns true when a running
195215 // session was cancelled; false if missing or already terminal.
196216 cancel ( id : string , reason ?: string ) : boolean ;
197- // Cancel every running session. Returns the ids that transitioned.
198- cancelAll ( reason ?: string ) : string [ ] ;
217+ // Cancel every running session. Closes retained workers with the same
218+ // deadline race as closeOne: leftover-child throws reject, hang-forever
219+ // resolves without throwing. Returns the ids that transitioned to cancelled.
220+ cancelAll ( reason ?: string ) : Promise < string [ ] > ;
199221 // CL-6943: flips a "pending_init" session to "running" once its agent
200222 // object actually exists. No-op on an unknown id or one already past init.
201223 markRunning ( id : string ) : void ;
@@ -1227,18 +1249,11 @@ export function createSubAgentSessionStore(
12271249 // close that does not honor its own deadline argument — a wedged
12281250 // descendant must not hang the whole close_agent call.
12291251 let closeError : unknown ;
1230- await Promise . race ( [
1231- close ( deadlineMs ) . then (
1232- ( ) => undefined ,
1233- ( err : unknown ) => {
1234- closeError = err ;
1235- log . warn ( "session close raced deadline: {error}" , {
1236- error : err instanceof Error ? err . message : String ( err ) ,
1237- } ) ;
1238- } ,
1239- ) ,
1240- new Promise < void > ( ( resolve ) => setTimeout ( resolve , deadlineMs ) ) ,
1241- ] ) ;
1252+ try {
1253+ await invokeCloseBounded ( close , deadlineMs ) ;
1254+ } catch ( err : unknown ) {
1255+ closeError = err ;
1256+ }
12421257 if ( keepFailed ) {
12431258 // fail() already stamped failed; invoke leftover teardown without
12441259 // rewriting that to shutdown.
@@ -1488,7 +1503,7 @@ export function createSubAgentSessionStore(
14881503 return cancelSession ( id , reason ) ;
14891504 } ,
14901505
1491- cancelAll ( reason = DEFAULT_CANCEL_REASON ) : string [ ] {
1506+ async cancelAll ( reason = DEFAULT_CANCEL_REASON ) : Promise < string [ ] > {
14921507 // Snapshot before cancelSession: markCancelled clears retained, and a
14931508 // resumed retained worker is strip-live so the first loop would otherwise
14941509 // skip the close-handle pass (CL-7001).
@@ -1500,10 +1515,17 @@ export function createSubAgentSessionStore(
15001515 for ( const session of running ) {
15011516 if ( cancelSession ( session . id , reason ) ) cancelled . push ( session . id ) ;
15021517 }
1518+ const pendingCloses : Promise < void > [ ] = [ ] ;
15031519 for ( const id of retainedIds ) {
15041520 const session = sessions . get ( id ) ;
15051521 if ( session === undefined || session . lifecycle . state === "shutdown" ) continue ;
1506- releaseHandles ( id ) ;
1522+ const close = closeHandles . get ( id ) ;
1523+ cancelAskInternal ( id , "session handles released" ) ;
1524+ closeHandles . delete ( id ) ;
1525+ cancelHandles . delete ( id ) ;
1526+ interruptHandles . delete ( id ) ;
1527+ followupHandles . delete ( id ) ;
1528+ deliverHandles . delete ( id ) ;
15071529 mutate ( id , ( s ) => {
15081530 s . lifecycle = {
15091531 state : "shutdown" ,
@@ -1512,6 +1534,15 @@ export function createSubAgentSessionStore(
15121534 } ;
15131535 s . retained = false ;
15141536 } ) ;
1537+ if ( close !== undefined ) {
1538+ pendingCloses . push ( invokeCloseBounded ( close , DEFAULT_CLOSE_DEADLINE_MS ) ) ;
1539+ }
1540+ }
1541+ const results = await Promise . allSettled ( pendingCloses ) ;
1542+ const failures = results . flatMap ( ( r ) => ( r . status === "rejected" ? [ r . reason ] : [ ] ) ) ;
1543+ if ( failures . length === 1 ) throw failures [ 0 ] ;
1544+ if ( failures . length > 1 ) {
1545+ throw new AggregateError ( failures , "session cancelAll close failed" ) ;
15151546 }
15161547 return cancelled ;
15171548 } ,
0 commit comments