@@ -60,11 +60,36 @@ export async function waitForBrokerEndpoint(endpoint, timeoutMs = 2000) {
6060 return false ;
6161}
6262
63- export async function sendBrokerShutdown ( endpoint ) {
63+ export async function sendBrokerShutdown ( endpoint , timeoutMs = 2000 ) {
6464 return new Promise ( ( resolve ) => {
65+ let settled = false ;
6566 const socket = connectToEndpoint ( endpoint ) ;
6667 let buffer = "" ;
6768 socket . setEncoding ( "utf8" ) ;
69+
70+ // Bound the wait so a wedged broker (connect accepted, but no data/close/error
71+ // ever arrives) can't hang the caller indefinitely — e.g. a runtime-identity
72+ // respawn that calls sendBrokerShutdown before teardown would otherwise stall
73+ // every subsequent command. Resolve false (treat as "broker not responsive")
74+ // and let the caller fall through to its teardown/kill path.
75+ const timer = setTimeout ( ( ) => {
76+ if ( settled ) {
77+ return ;
78+ }
79+ settled = true ;
80+ socket . destroy ( ) ;
81+ resolve ( false ) ;
82+ } , timeoutMs ) ;
83+
84+ const finish = ( value ) => {
85+ if ( settled ) {
86+ return ;
87+ }
88+ settled = true ;
89+ clearTimeout ( timer ) ;
90+ resolve ( value ) ;
91+ } ;
92+
6893 socket . on ( "connect" , ( ) => {
6994 socket . write ( `${ JSON . stringify ( { id : 1 , method : "broker/shutdown" , params : { } } ) } \n` ) ;
7095 } ) ;
@@ -77,13 +102,13 @@ export async function sendBrokerShutdown(endpoint) {
77102 const line = buffer . slice ( 0 , newlineIndex ) ;
78103 socket . end ( ) ;
79104 try {
80- resolve ( ! JSON . parse ( line . trim ( ) ) . error ) ;
105+ finish ( ! JSON . parse ( line . trim ( ) ) . error ) ;
81106 } catch {
82- resolve ( false ) ;
107+ finish ( false ) ;
83108 }
84109 } ) ;
85- socket . on ( "error" , ( ) => resolve ( false ) ) ;
86- socket . on ( "close" , ( ) => resolve ( false ) ) ;
110+ socket . on ( "error" , ( ) => finish ( false ) ) ;
111+ socket . on ( "close" , ( ) => finish ( false ) ) ;
87112 } ) ;
88113}
89114
@@ -165,7 +190,13 @@ async function loadReusableBrokerSessionUnlocked(cwd, options = {}) {
165190 }
166191
167192 if ( existing ) {
168- if ( await isBrokerEndpointReady ( existing . endpoint ) ) {
193+ // Only trust the recorded pid for tree-kill when the endpoint probe confirmed
194+ // the broker was actually live. A stale session whose endpoint is not ready
195+ // likely points at a dead broker whose pid the OS may have recycled into an
196+ // unrelated process — tree-killing there risks killing the wrong process, so
197+ // just drop the files and let any survivor exit on its own.
198+ const existingReady = await isBrokerEndpointReady ( existing . endpoint ) ;
199+ if ( existingReady ) {
169200 const brokerStatus = await probeBroker ( existing . endpoint , cwd ) ;
170201 if ( brokerStatus === "busy" && options . allowBusyStaleBroker ) {
171202 return existing ;
@@ -179,7 +210,10 @@ async function loadReusableBrokerSessionUnlocked(cwd, options = {}) {
179210 return null ;
180211 }
181212 }
182- teardownExistingBroker ( cwd , existing , options . killProcess ?? terminateProcessTree ) ;
213+ const killProcess = existingReady
214+ ? ( options . killProcess ?? terminateProcessTree )
215+ : ( options . killProcess ?? null ) ;
216+ teardownExistingBroker ( cwd , existing , killProcess ) ;
183217 }
184218
185219 return null ;
0 commit comments