Repository navigation
fix: bound turn/interrupt with a timeout so cancellation can't hang the companion - #48
Conversation
…he companion
client.request("turn/interrupt", ...) had no timeoutMs, so a broker that
accepted the connection and then went silent would block the companion
until the host's own SIGKILL, leaving the original write-capable turn
running with nobody able to reach it. Pass a 5s timeoutMs (with onTimeout
force-closing the connection), mirroring the pattern already used for
the initialize handshake.
Fixes #47 (finding 2; the cheapest, highest-value item from the proposed
fix list).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013hBHjrDA8DzzY2SEq4ieiS
🔍 Local review (cycle 1) — round 744d840d-f015-48ca-802b-fe7fe950b2a3Reviewed locally (
No critical issues found. One non-blocking suggestion noted, left as-is (does not affect correctness). |
📋 Review summary — all cycles
Totals: 1 FIX (resolved before review, in the PR itself), 1 SKIP. Note: Codex companion was silent this round (usage-limit error, resets ~Aug 8 2026) and was excluded from this run per explicit user decision; the review-only reviewer for this cycle was |
Codex review of PR #48 (issue #47 finding 2) caught that interruptAppServerTurn's onTimeout handler called client.close(), but BrokerCodexAppServerClient.close() sent socket.end() and awaited exitPromise with no fallback. A peer that accepts the FIN but never reciprocates leaves exitPromise unresolved forever, so close() could hang indefinitely — defeating the request-level timeoutMs the previous commit added, since the cleanup path it triggers wasn't itself bounded. Add a 50ms grace period before force-destroying the socket, mirroring the pattern the spawned client's close() already uses (stdin.end() + killChildNow() fallback). New fixture spawnStubbornBroker() completes the handshake and answers requests, but (via allowHalfOpen: true) never reciprocates the FIN -- reproducing the exact wedge Codex described. Verified red without the fix (hangs ~3s) and green with it (~56ms, matching the grace period). Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_013hBHjrDA8DzzY2SEq4ieiS
|
Thanks — this is a real bug, fixed in 2320230.
Fix: Added a regression test ( |
🔍 Local review (cycle 2) — Codex re-reviewRe-reviewed by Codex against the fix commit (2320230): verdict 📋 Review summary — all cycles
Totals: 1 FIX (resolved), 1 SKIP, cycle 2 clean (approve, no new findings). CI: no checks configured on this repo/branch. |
Summary
interruptAppServerTurn(plugins/codex/scripts/lib/codex.mjs:1206) calledclient.request("turn/interrupt", ...)with notimeoutMs. If a broker accepted the connection and then went silent, the companion would block on thatawaituntil the host's own SIGKILL — leaving the original write-capable turn running on the broker with nobody able to reach it.TURN_INTERRUPT_TIMEOUT_MS = 5000and passestimeoutMs+onTimeout(force-close viaclient.close()) to the request, mirroring the existing pattern used for theinitializehandshake (app-server.mjs:380-389).Finding 1 (deadline anchored after an unbounded setup phase) and the remaining proposed items are a larger design change (moving where the ceiling is armed) and are left for a follow-up, as the issue itself scopes them separately.
Test plan
silent-turn-interruptintests/fake-codex-fixture.mjs: acceptsturn/interruptbut never answers it.tests/runtime.test.mjs:turn/interrupt itself is bounded: a broker that accepts it but never answers does not hang the companion (#47)— asserts the companion exits within ceiling + interrupt timeout (~9s) rather than hanging on the unanswered request.npm test— 185/185 passing, 0 regressions.Fixes #47 (finding 2).
🤖 Generated with Claude Code
https://claude.ai/code/session_013hBHjrDA8DzzY2SEq4ieiS