Environment
- Android aarch64, Termux — a supported platform with official docs (
packages/coding-agent/docs/termux.md; Termux is listed in the README's Platform notes)
- Source install, Node v24, daemon mode
Symptom
Resuming a session whose worker has been stopped (e.g. by idle eviction) always fails with:
SessionAlreadyActiveError: Session is already active in <activeSessionId>: .../<session>.jsonl
even though the worker process that owned the session lease is long dead (SIGKILLed, descriptor cleaned up, socket gone).
Root cause
Three layers combine to make the lease liveness check permanently report "alive":
-
SessionLease (src/core/session-lease.ts) is released only on graceful shutdown (release()). A worker killed with SIGKILL (e.g. idle eviction that did not stop cleanly, later adopted with SIGKILL on daemon restart) leaves ~/.prime/agent/session-leases/<hash>.lock/owner.json behind.
-
isProcessAlive (session-lease.ts:104) uses process.kill(pid, 0). On Android/Termux a dead pid that has been reused by another (SELinux-protected) process returns EPERM, not ESRCH, and the code conservatively treats EPERM as alive:
function isProcessAlive(pid: number): boolean {
try { process.kill(pid, 0); return true; }
catch (error) { return (error as NodeJS.ErrnoException).code === "EPERM"; }
}
-
The second guard isLeaseOwnerAlive (session-lease.ts:177) compares processStartId read from /proc/<pid>/stat. On Termux the reused pid's /proc/<pid>/stat is invisible (ENOENT), the ps fallback also returns nothing, so getProcessStartId returns undefined — and undefined is treated as alive:
const currentStartId = getProcessStartId(owner.pid);
return currentStartId === undefined || currentStartId === owner.processStartId;
Result: acquireSessionLease never reaches reclaimStaleLease and the session can never be resumed without manually deleting the lease directory.
Verified on device
pid 99999 (never existed) -> kill(0) ESRCH /proc/<pid>/stat ENOENT
pid 26617 (dead, pid reused) -> kill(0) EPERM ❌ /proc/<pid>/stat ENOENT ✅
pid 29551/30447 (alive, same uid) -> kill(0) OK /proc/<pid>/stat OK
So /proc/<pid>/stat's ENOENT semantics are reliable for same-uid processes where kill(0) EPERM is not.
Suggested fixes
Minimal (3 lines): in isProcessAlive, on EPERM also check /proc/<pid>/stat; if it is ENOENT, treat the process as dead (falling back to the current conservative behavior only when the proc entry is readable).
Robust (recommended): make the lease an ownership lock held with flock/fcntl (e.g. keep the lock file open for the lease duration). The kernel guarantees the lock is released when the owning process dies — including SIGKILL — with no dependency on pid semantics, /proc visibility, or kill(0) behavior. Verified working on Termux: a SIGKILLed flock holder's lock is released immediately and a new acquirer succeeds.
Related issues
Same stale-session-lease failure family, different platform-specific fault point:
A robust fix that works everywhere would be to hold the lease as an flock/fcntl ownership lock (kernel releases it automatically when the owning process dies, independent of pid semantics and /proc visibility).
Environment
packages/coding-agent/docs/termux.md; Termux is listed in the README's Platform notes)Symptom
Resuming a session whose worker has been stopped (e.g. by idle eviction) always fails with:
even though the worker process that owned the session lease is long dead (SIGKILLed, descriptor cleaned up, socket gone).
Root cause
Three layers combine to make the lease liveness check permanently report "alive":
SessionLease(src/core/session-lease.ts) is released only on graceful shutdown (release()). A worker killed with SIGKILL (e.g. idle eviction that did not stop cleanly, later adopted with SIGKILL on daemon restart) leaves~/.prime/agent/session-leases/<hash>.lock/owner.jsonbehind.isProcessAlive(session-lease.ts:104) usesprocess.kill(pid, 0). On Android/Termux a dead pid that has been reused by another (SELinux-protected) process returns EPERM, not ESRCH, and the code conservatively treats EPERM as alive:The second guard
isLeaseOwnerAlive(session-lease.ts:177) comparesprocessStartIdread from/proc/<pid>/stat. On Termux the reused pid's/proc/<pid>/statis invisible (ENOENT), thepsfallback also returns nothing, sogetProcessStartIdreturnsundefined— andundefinedis treated as alive:Result:
acquireSessionLeasenever reachesreclaimStaleLeaseand the session can never be resumed without manually deleting the lease directory.Verified on device
So
/proc/<pid>/stat's ENOENT semantics are reliable for same-uid processes wherekill(0)EPERM is not.Suggested fixes
Minimal (3 lines): in
isProcessAlive, on EPERM also check/proc/<pid>/stat; if it is ENOENT, treat the process as dead (falling back to the current conservative behavior only when the proc entry is readable).Robust (recommended): make the lease an ownership lock held with
flock/fcntl(e.g. keep the lock file open for the lease duration). The kernel guarantees the lock is released when the owning process dies — including SIGKILL — with no dependency on pid semantics,/procvisibility, orkill(0)behavior. Verified working on Termux: a SIGKILLed flock holder's lock is released immediately and a new acquirer succeeds.Related issues
Same stale-session-lease failure family, different platform-specific fault point:
renameSynconto an existing lease directory returnsEPERM/EACCESon Windows instead of POSIXEEXIST/ENOTEMPTY, soreclaimStaleLeaseis never reached. PR fix(coding-agent): reclaim stale session leases on Windows #727 fixes that withisRenameTargetExistsError.isLeaseOwnerAlivemisjudges a dead owner as alive —kill(pid, 0)returnsEPERMfor a pid reused by a SELinux-protected process, and the/procstarttime fallback returnsundefined, which is also treated as alive. SoreclaimStaleLeaseis again never reached, and PR fix(coding-agent): reclaim stale session leases on Windows #727 does not cover this case (the failing check here is inisLeaseOwnerAlive, not in the rename error handling).A robust fix that works everywhere would be to hold the lease as an
flock/fcntlownership lock (kernel releases it automatically when the owning process dies, independent of pid semantics and/procvisibility).