fix(codex): stop a surviving descendant wedging the Codex home process lock forever - #12410
Conversation
…s lock The per-Codex-home process lock was released only by the child's 'close' event. 'close' fires when the child AND every process that inherited its stdio have exited, so a grandchild that outlives codex (an MCP server, a helper, an `sh -c` wrapper) keeps the pipe open and 'close' never arrives. killProcessTree only SIGKILLs the direct child on POSIX, so Stop and the 60s generation timeout both leave the lock held. Every later AI commit message, PR field, branch name, and quota probe for that home then queued forever with no error and no recovery until the app restarted. Release the hold on 'exit' as well as 'close': once the codex process itself is gone it can no longer rotate that home's auth.json, which is the race the lock exists to prevent, so waiting on descendants bought nothing. As a backstop for a child that never reports exit at all (a kill that does not land), bound the hold: while another run is queued behind it, a hold that outlives CODEX_HOME_PROCESS_LOCK_MAX_HOLD_MS releases the queue. The cap is armed only when someone is waiting and only counts time the entry actually holds the lock, so uncontended runs arm no timer and queueing behind a slow predecessor never shortens a run's own budget.
📝 WalkthroughWalkthroughThe change updates Codex process-home lock tracking to recognize child-process 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Electron QA — live app, this branchRan a dev build off this branch ( Cancel → regenerate on the same The run cancelled mid-flight (so Generation in flight (stop control, staged changes): Generated message landed in the commit box: What this does and does not proveBeing explicit, because it matters for how much weight to put on the screenshots: on macOS a plain cancel usually leaves no surviving descendant holding the child's stdio, so The discriminating evidence is the unit tests. Reverting only the two source files to restoring them: Worth noting the failure shape: it is not confined to the new cases. Because Note on a lane the description does not mention
Not covered here: Windows and WSL process teardown, and the actual surviving-MCP-descendant scenario against a real Codex binary. |
Electron QAValidated the current PR head (
|




Defect
The per-Codex-home process lock (
withCodexHomeProcessLock) is held untilLocalProcessExecution.processClosedresolves, and that promise was resolved only from the child'scloseevent (child.once('close', markClosedAfterTermination)in bothrunLocalPlananddiscoverCommitMessageModelsLocal).closefires only when the child and every process that inherited its stdio have exited.killProcessTreesendsSIGKILLto the direct child on POSIX, which does not reach a grandchild that inherited stdout — an MCP server, a helper process, or ansh -cwrapper. The codex process dies, the descendant keeps the pipe open,closenever fires, and the lock is never released.Failure scenario
GENERATION_TIMEOUT_MSfires).killProcessTreeSIGKILLs codex; the MCP grandchild survives.exit;closenever arrives.CODEX_HOMEqueues behind the never-settling hold. No error, no spinner end, no recovery — the feature is dead until the app restarts.The same wedge existed on the model-discovery path, which holds the same lock.
Fix
exitas well asclose. Once the codex process itself is gone it can no longer rotate that home'sauth.json— the exact race the lock exists to prevent — so waiting on unrelated descendants bought no safety. RenamedholdHomeLockUntilClosetoholdHomeLockUntilExitto match.closeas a fallback and preserve the existing Windows tree-termination wait, so wrapper/process-tree cleanup still completes before the next run starts.Verification
New tests, each confirmed to fail with the source change reverted and pass with it restored:
commit-message-text-generation.test.ts— "releases the Codex home lock when the child exits with a descendant holding its stdio": cancel a Codex run, emit onlyexit, and assert the next Codex run for the same home still spawns. Without the fix it never spawns.commit-message-text-generation.test.ts— "releases the Codex home after a discovery timeout once the child exits": same for the model-discovery lane.codex-home-process-lock.test.ts— "does not release a running lock based on elapsed time": advance an hour while another run is queued and assert strict serialization remains intact.Run:
npx vitest run --config config/vitest.config.ts src/main/codex-cli/ src/main/rate-limits/ src/main/text-generation/— 36 files, 531 tests pass.npx tsc --noEmit -p config/tsconfig.node.json,oxlint,oxfmt, reliability gates, and the max-lines ratchet are clean.