diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f7100b579..091e5fbe5 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ - Added privacy-safe pseudonymous product analytics for onboarding, command use, execution modes, run outcomes, TTFT, latency, usage, tools, retries, and compactions, with disclosure and opt-out controls ([ENG-4682](https://linear.app/primeintellect/issue/ENG-4682/add-privacy-safe-posthog-analytics-to-prime-agent)). - Changed sent agent messages in the IPython cell UI to show only the message text with a `╰─` gutter when expanded, matching received messages, and hid the raw `agent_message.send` receipt dictionary. - Fixed Homebrew installs attempting to self-update their versioned Cellar keg instead of directing users to `brew upgrade prime-agent` ([#844](https://github.com/PrimeIntellect-ai/prime-agent/issues/844)) +- Fixed stale session leases becoming permanent on Android/Termux when a dead worker PID is reused by a protected process ([#868](https://github.com/PrimeIntellect-ai/prime-agent/issues/868)). ## [0.7.1] - 2026-08-07 diff --git a/packages/coding-agent/src/core/session-lease.ts b/packages/coding-agent/src/core/session-lease.ts index 6c4e2975c..ffb24b076 100644 --- a/packages/coding-agent/src/core/session-lease.ts +++ b/packages/coding-agent/src/core/session-lease.ts @@ -101,12 +101,38 @@ function readLeaseOwner(directory: string): SessionLeaseOwner | undefined { } } -function isProcessAlive(pid: number): boolean { +type ProcessProbe = (pid: number) => void; + +const probeProcessSignal: ProcessProbe = (pid) => { + process.kill(pid, 0); +}; + +const probeProcStat: ProcessProbe = (pid) => { + readFileSync(`/proc/${pid}/stat`); +}; + +export function isProcessAlive( + pid: number, + signalProbe: ProcessProbe = probeProcessSignal, + procStatProbe: ProcessProbe = probeProcStat, + platform: NodeJS.Platform = process.platform, +): boolean { + try { + signalProbe(pid); + return true; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "EPERM") { + return false; + } + } + if (platform !== "linux" && platform !== "android") { + return true; + } try { - process.kill(pid, 0); + procStatProbe(pid); return true; } catch (error) { - return (error as NodeJS.ErrnoException).code === "EPERM"; + return (error as NodeJS.ErrnoException).code !== "ENOENT"; } } diff --git a/packages/coding-agent/test/session-lease.test.ts b/packages/coding-agent/test/session-lease.test.ts index 87338b686..36505597a 100644 --- a/packages/coding-agent/test/session-lease.test.ts +++ b/packages/coding-agent/test/session-lease.test.ts @@ -8,6 +8,7 @@ import { acquireSessionLease, canonicalSessionPath, getWindowsProcessStartId, + isProcessAlive, SESSION_LEASE_OWNER_ID_ENV, SESSION_LEASES_ENABLED_ENV, SessionAlreadyActiveError, @@ -34,6 +35,76 @@ function enabledEnvironment(owner: string): NodeJS.ProcessEnv { }; } +function errno(code: string): NodeJS.ErrnoException { + return Object.assign(new Error(code), { code }); +} + +describe("process liveness", () => { + it.each(["linux", "android"] as const)( + "treats EPERM with a missing proc entry as a stale or reused pid on %s", + (platform) => { + expect( + isProcessAlive( + 42, + () => { + throw errno("EPERM"); + }, + () => { + throw errno("ENOENT"); + }, + platform, + ), + ).toBe(false); + }, + ); + + it("keeps treating EPERM as alive when the proc entry is readable", () => { + expect( + isProcessAlive( + 42, + () => { + throw errno("EPERM"); + }, + () => {}, + "android", + ), + ).toBe(true); + }); + + it("stays conservative when procfs fails for a reason other than a missing entry", () => { + expect( + isProcessAlive( + 42, + () => { + throw errno("EPERM"); + }, + () => { + throw errno("EACCES"); + }, + "android", + ), + ).toBe(true); + }); + + it("does not use procfs to reinterpret EPERM on non-procfs platforms", () => { + let procProbeCalls = 0; + expect( + isProcessAlive( + 42, + () => { + throw errno("EPERM"); + }, + () => { + procProbeCalls++; + throw errno("ENOENT"); + }, + "darwin", + ), + ).toBe(true); + expect(procProbeCalls).toBe(0); + }); +}); + describe("session leases", () => { it("reads an invariant process start identity on Windows", () => { const calls: Array<{ command: string; args: string[] }> = [];