diff --git a/packages/code-runtime/code-runtime-python/src/index.ts b/packages/code-runtime/code-runtime-python/src/index.ts index 1ab308a163..604cc420b9 100644 --- a/packages/code-runtime/code-runtime-python/src/index.ts +++ b/packages/code-runtime/code-runtime-python/src/index.ts @@ -1416,9 +1416,18 @@ export class PythonCodeRuntime extends CodeRuntime { if (child.pid === undefined) return // A pid alone cannot answer this: `process.kill(pid, 0)` succeeds just // as well for a REPLACEMENT process holding the recycled number. Only - // the start time distinguishes the two, so a reading that no longer - // matches means the group is not this run's and must not be signalled. - if (leaderStarted !== undefined && readProcessStart(child.pid) !== leaderStarted) return + // the start time distinguishes the two, so a reading that DISAGREES + // means the number now belongs to another process and must not be + // signalled. + // + // An ABSENT reading is the ordinary case, not a mismatch: once the + // leader is reaped its `/proc//stat` is gone, while the group it + // led can still hold survivors that this teardown exists to reap. So + // only a present-and-different reading blocks the signal; undefined + // falls through, which is also the behavior on platforms with no + // `/proc` to read. + const nowStarted = readProcessStart(child.pid) + if (leaderStarted !== undefined && nowStarted !== undefined && nowStarted !== leaderStarted) return process.kill(-child.pid, sig) } catch { // ESRCH — the process already died. Nothing to do. diff --git a/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts b/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts index b3a46cee21..a674056ece 100644 --- a/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts +++ b/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts @@ -399,7 +399,13 @@ describe('PythonCodeRuntime — process identity', () => { expect(own).toBeDefined() expect(readProcessStart(process.pid)).toBe(own) // Pid 0 is never a readable /proc entry, so the guard degrades to - // undefined rather than throwing on a teardown path. + // undefined rather than throwing on a teardown path. This is also the + // reading a REAPED leader produces -- its /proc entry is gone while the + // group it led can still hold survivors -- so `undefined` must NOT be + // treated as an identity mismatch. Reading it as one refused the SIGKILL + // that the same-group survivor tests depend on, which is why they went red + // on Linux while passing on Darwin (where the reader always returns + // undefined and the guard is inert). expect(readProcessStart(0)).toBeUndefined() } else { // Darwin has no /proc: the reader reports undefined, and `killGroup` then