Keep the inferred-completion timer referenced - #696
Open
stevebooks wants to merge 1 commit into
Open
Conversation
The timer scheduled by scheduleInferredCompletion is the only thing that resolves state.completion when a turn arrives without an explicit final turn marker. Because it was unref'd it did not hold the event loop open, so once the app-server socket closed the loop could drain with `await state.completion` still pending. Node then exited 0 having written nothing to stdout or stderr. Callers that expect JSON on stdout cannot tell this apart from a real result. The stop-review gate parses that empty stdout and reports "the stop-time Codex review task returned invalid JSON", which surfaces to the user as a failed review rather than a tooling failure, blocking the turn. The timer is bounded at 250ms and is always cleared, so keeping it referenced cannot delay or hang shutdown.
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.
Problem
codex-companion.mjscan exit0having written nothing at all — no stdout, no stderr. Any caller that expects JSON on stdout cannot distinguish this from success.The visible symptom is in the stop-review gate:
stop-review-gate-hook.mjsrunscodex-companion.mjs task --json, gets zero bytes, andJSON.parsethrows. The hook reports "the stop-time Codex review task returned invalid JSON" and blocks the turn, so a tooling failure is presented to the user as a failed code review.I have 27 occurrences logged locally over ~2.5 months, all with an identical signature:
No signal, no error code, both streams empty.
Cause
scheduleInferredCompletioninplugins/codex/scripts/lib/codex.mjsschedules the 250ms timer that resolvesstate.completionwhen a turn arrives without an explicit final-turn marker. That timer wasunref'd, so it does not hold the event loop open:Meanwhile
runAppServerTurnis parked onawait state.completion. Once the app-server socket closes, the unref'd timer is the only handle left — and unref'd handles do not keep Node alive. The loop drains, the promise never settles, and Node exits0silently. An unresolved promise is not an error, so nothing is reported.It is a race against socket teardown, which is why it is intermittent rather than constant.
Notably this is not a broker failure. I tested both broker failure modes against the unmodified client, and neither produces this signature:
1withcodex app-server connection closedon stderr (handleExitcorrectly rejects pending requests)Only event-loop drain yields exit
0with both streams empty.Change
Drop the
.unref?.()so the timer stays referenced. The timer is bounded at 250ms and is always cleared (clearCompletionTimer, plus it nulls itself in its own callback), so keeping it referenced cannot delay or hang shutdown. Its early-return branches only trigger whenstate.completedis already true or when work is still pending — in which case a later event reschedules.Test
tests/inferred-completion.test.mjscovers the observable contract in a subprocess, sincescheduleInferredCompletionis not exported:0with empty stdout (pins the failure mode)scheduleInferredCompletiondoes not unref its timer (guards the regression)The third test fails against
mainbefore the change and passes after.Verified end-to-end against a live broker:
task --jsonnow returns 151 bytes of valid JSON withrawOutputpopulated, where it previously returned zero bytes.Notes
tests/state.test.mjs→ "resolveStateDir uses a temp-backed per-workspace directory" already fails on a clean checkout ofmainon macOS, before this change. It looks environment-specific and is untouched here. Excluding it, the surrounding suite is 20/21 with the 3 new tests passing. I did not run the full suite locally as it spawns real codex processes and exceeds my local timeout.