Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
32 changes: 29 additions & 3 deletions packages/coding-agent/src/core/session-lease.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}

Expand Down
71 changes: 71 additions & 0 deletions packages/coding-agent/test/session-lease.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
acquireSessionLease,
canonicalSessionPath,
getWindowsProcessStartId,
isProcessAlive,
SESSION_LEASE_OWNER_ID_ENV,
SESSION_LEASES_ENABLED_ENV,
SessionAlreadyActiveError,
Expand All @@ -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[] }> = [];
Expand Down