@@ -187,6 +187,15 @@ export function getSelfAndAncestorPidsSync(spawnTimeoutMs = SPAWN_TIMEOUT_MS): S
187187 return pids ;
188188}
189189
190+ function getExcludedGatewayPidsSync ( spawnTimeoutMs : number , protectedPid ?: number ) : Set < number > {
191+ const excluded = getSelfAndAncestorPidsSync ( spawnTimeoutMs ) ;
192+ if ( typeof protectedPid === "number" && Number . isSafeInteger ( protectedPid ) && protectedPid > 0 ) {
193+ // A reparented service can become a sibling and disappear from the ancestor walk.
194+ excluded . add ( protectedPid ) ;
195+ }
196+ return excluded ;
197+ }
198+
190199/**
191200 * Parse raw PIDs from lsof -Fpc stdout, excluding the current
192201 * process and its ancestors (see `getSelfAndAncestorPidsSync` for the full
@@ -257,12 +266,16 @@ function verifyGatewayPidByArgvSync(pid: number, spawnTimeoutMs: number): boolea
257266 return args != null && isGatewayArgv ( args , { allowGatewayBinary : true } ) ;
258267}
259268
260- function parsePidsFromLsofOutput ( stdout : string , spawnTimeoutMs : number ) : number [ ] {
269+ function parsePidsFromLsofOutput (
270+ stdout : string ,
271+ spawnTimeoutMs : number ,
272+ protectedPid ?: number ,
273+ ) : number [ ] {
261274 // Deduplicate: dual-stack listeners (IPv4 + IPv6) cause lsof to emit the
262275 // same PID twice. Return each PID at most once to avoid double-killing.
263276 // Exclude self and ancestors — terminating any ancestor cascade-kills the
264277 // caller via the supervisor, recreating the #68451 restart loop.
265- const excluded = getSelfAndAncestorPidsSync ( spawnTimeoutMs ) ;
278+ const excluded = getExcludedGatewayPidsSync ( spawnTimeoutMs , protectedPid ) ;
266279 const pids : number [ ] = [ ] ;
267280 for ( const entry of parseLsofEntries ( stdout ) ) {
268281 if ( excluded . has ( entry . pid ) ) {
@@ -285,8 +298,8 @@ function parsePidsFromLsofOutput(stdout: string, spawnTimeoutMs: number): number
285298 * and its ancestors (same invariant as the lsof path — see
286299 * `getSelfAndAncestorPidsSync`).
287300 */
288- function filterVerifiedWindowsGatewayPids ( rawPids : number [ ] ) : number [ ] {
289- const excluded = getSelfAndAncestorPidsSync ( ) ;
301+ function filterVerifiedWindowsGatewayPids ( rawPids : number [ ] , protectedPid ?: number ) : number [ ] {
302+ const excluded = getExcludedGatewayPidsSync ( SPAWN_TIMEOUT_MS , protectedPid ) ;
290303 return uniqueValues ( rawPids )
291304 . filter ( ( pid ) => Number . isFinite ( pid ) && pid > 0 && ! excluded . has ( pid ) )
292305 . filter ( ( pid ) => {
@@ -298,8 +311,9 @@ function filterVerifiedWindowsGatewayPids(rawPids: number[]): number[] {
298311function filterVerifiedWindowsGatewayPidsResult (
299312 rawPids : number [ ] ,
300313 processArgsResult : ( pid : number ) => WindowsProcessArgsResult ,
314+ protectedPid ?: number ,
301315) : WindowsListeningPidsResult {
302- const excluded = getSelfAndAncestorPidsSync ( ) ;
316+ const excluded = getExcludedGatewayPidsSync ( SPAWN_TIMEOUT_MS , protectedPid ) ;
303317 const verified : number [ ] = [ ] ;
304318 for ( const pid of uniqueValues ( rawPids ) ) {
305319 if ( ! Number . isFinite ( pid ) || pid <= 0 || excluded . has ( pid ) ) {
@@ -316,32 +330,34 @@ function filterVerifiedWindowsGatewayPidsResult(
316330 return { ok : true , pids : verified } ;
317331}
318332
319- function findVerifiedWindowsGatewayPidsOnPortSync ( port : number ) : number [ ] {
320- return filterVerifiedWindowsGatewayPids ( readWindowsListeningPidsOnPortSync ( port ) ) ;
333+ function findVerifiedWindowsGatewayPidsOnPortSync ( port : number , protectedPid ?: number ) : number [ ] {
334+ return filterVerifiedWindowsGatewayPids ( readWindowsListeningPidsOnPortSync ( port ) , protectedPid ) ;
321335}
322336
323- function findVerifiedWindowsGatewayPidsOnPortResultSync ( port : number ) : WindowsListeningPidsResult {
337+ function findVerifiedWindowsGatewayPidsOnPortResultSync (
338+ port : number ,
339+ protectedPid ?: number ,
340+ ) : WindowsListeningPidsResult {
324341 const result = readWindowsListeningPidsResultSync ( port ) ;
325342 if ( ! result . ok ) {
326343 return result ;
327344 }
328- return filterVerifiedWindowsGatewayPidsResult ( result . pids , ( pid ) =>
329- readWindowsProcessArgsResultSync ( pid ) ,
345+ return filterVerifiedWindowsGatewayPidsResult (
346+ result . pids ,
347+ ( pid ) => readWindowsProcessArgsResultSync ( pid ) ,
348+ protectedPid ,
330349 ) ;
331350}
332351
333- /**
334- * Find PIDs of gateway processes listening on the given port using synchronous lsof.
335- * Returns only PIDs that belong to openclaw gateway processes (not the current process).
336- */
337- export function findGatewayPidsOnPortSync (
352+ function findGatewayPidsOnPortWithProtectedPidSync (
338353 port : number ,
339- spawnTimeoutMs = SPAWN_TIMEOUT_MS ,
354+ spawnTimeoutMs : number ,
355+ protectedPid ?: number ,
340356) : number [ ] {
341357 if ( process . platform === "win32" ) {
342358 // Use the shared Windows port inspection (PowerShell / netstat) with
343359 // command-line verification to find only openclaw gateway processes.
344- return findVerifiedWindowsGatewayPidsOnPortSync ( port ) ;
360+ return findVerifiedWindowsGatewayPidsOnPortSync ( port , protectedPid ) ;
345361 }
346362 const lsof = resolveLsofCommandSync ( ) ;
347363 const res = spawnSync ( lsof , [ "-nP" , `-iTCP:${ port } ` , "-sTCP:LISTEN" , "-Fpc" ] , {
@@ -368,7 +384,18 @@ export function findGatewayPidsOnPortSync(
368384 ) ;
369385 return [ ] ;
370386 }
371- return parsePidsFromLsofOutput ( res . stdout , spawnTimeoutMs ) ;
387+ return parsePidsFromLsofOutput ( res . stdout , spawnTimeoutMs , protectedPid ) ;
388+ }
389+
390+ /**
391+ * Find PIDs of gateway processes listening on the given port using synchronous lsof.
392+ * Returns only PIDs that belong to openclaw gateway processes (not the current process).
393+ */
394+ export function findGatewayPidsOnPortSync (
395+ port : number ,
396+ spawnTimeoutMs = SPAWN_TIMEOUT_MS ,
397+ ) : number [ ] {
398+ return findGatewayPidsOnPortWithProtectedPidSync ( port , spawnTimeoutMs ) ;
372399}
373400
374401/**
@@ -586,23 +613,31 @@ function waitForPortFreeSync(port: number): void {
586613 *
587614 * Called before service restart commands to prevent port conflicts.
588615 */
589- export function cleanStaleGatewayProcessesSync ( portOverride ?: number ) : number [ ] {
616+ type CleanStaleGatewayProcessesOptions = {
617+ protectedPid ?: number ;
618+ } ;
619+
620+ export function cleanStaleGatewayProcessesSync (
621+ portOverride ?: number ,
622+ options ?: CleanStaleGatewayProcessesOptions ,
623+ ) : number [ ] {
590624 try {
591625 const port =
592626 typeof portOverride === "number" && Number . isFinite ( portOverride ) && portOverride > 0
593627 ? Math . floor ( portOverride )
594628 : resolveGatewayPort ( undefined , process . env ) ;
629+ const protectedPid = options ?. protectedPid ;
595630 const stalePids =
596631 process . platform === "win32"
597632 ? ( ( ) => {
598- const result = findVerifiedWindowsGatewayPidsOnPortResultSync ( port ) ;
633+ const result = findVerifiedWindowsGatewayPidsOnPortResultSync ( port , protectedPid ) ;
599634 if ( result . ok ) {
600635 return result . pids ;
601636 }
602637 waitForPortFreeSync ( port ) ;
603638 return [ ] ;
604639 } ) ( )
605- : findGatewayPidsOnPortSync ( port ) ;
640+ : findGatewayPidsOnPortWithProtectedPidSync ( port , SPAWN_TIMEOUT_MS , protectedPid ) ;
606641 if ( stalePids . length === 0 ) {
607642 return [ ] ;
608643 }
0 commit comments