fix: preserve sandboxed background jobs on EPERM - #80
Open
behruznassre wants to merge 1 commit into
Open
Conversation
Treat permission-denied zero-signal probes as evidence that a process exists so the stale-job reaper does not fail live Claude jobs. Add deterministic EPERM and ESRCH coverage.\n\nRefs sendbird#79
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
EPERMESRCHand other probe failures classified as not aliveRoot cause
isProcessAlive()treated everyprocess.kill(pid, 0)exception as proof that the process was dead.reapStaleJobs()then moved the job tofailed; the later successful completion CAS respected that terminal state, leaving the complete review only in the log.The denial is scoped to non-descendant processes. Seatbelt denies signalling processes outside the sandbox, and
reapStaleJobs()runs in a different invocation from the one that spawned the job — so the PID it probes is never its own child, and getsEPERMwhile the job is still running. A process probing its own child never reproduces this.The zero-signal probe already sits at the correct shared boundary. This change preserves its current boolean API and recognizes the POSIX distinction:
EPERMmeans the process exists but cannot be signaled, whileESRCHmeans it does not exist.Validation
Probed inside a real Codex
workspace-writesandbox (macOS 25.5.0, codex-cli 0.145.0), with an unsandboxed baseline on the same machine:spawnSync psRow 2 is the bug. Row 4 is why this fix is safe: a dead PID still returns
ESRCHinside the sandbox, because the kernel resolves process existence before the sandbox policy denies the signal.EPERM → alivetherefore cannot mask a genuinely dead process, and stale jobs are still reaped normally — no unboundedrunningstate is introduced.REAP_GRACE_MSis 2s with no max-duration bound, so that distinction is load-bearing.Also:
reapStaleJobs()path with anEPERMPID retainedstatus: running,phase: running, and no error.npm run checkAn earlier revision of this description cited
process.kill(1, 0)returningEPERMas sandbox evidence. That was wrong and has been replaced:kill(1, 0)returnsEPERMon plain macOS with no sandbox, because a non-root user cannot signal root'sinit. The conclusion was right, the evidence was not.Not addressed here
validateProcessIdentity()has the same defect — a barecatchreturningfalse, wrappinggetProcessIdentity()→spawnSync ps, which the sandbox denies unconditionally (last row above).reapStaleJobs()prefers that branch wheneverjob.pidIdentityis set. It is unreachable today only becauseclaude-cli.mjsswallows the identicalpsfailure at spawn and leavespidIdentity: null, so the reaper falls back toisProcessAlive()and this fix applies. Tracked separately rather than widened into this PR.Closes #79