Skip to content

Commit 2eefec2

Browse files
declan-scaleclaude
andcommitted
fix(cli): reap codex subprocess on failure in codex templates
Address Greptile review: wrap the streaming call in try/finally across the default, sync and temporal codex templates so the codex subprocess is killed and reaped even when auto_send_turn / yield_turn raises or the async generator is abandoned. Previously a failed turn left codex blocked on a full stdout pipe buffer, leaking an OS process per failure until the server/worker restarted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 921ee17 commit 2eefec2

3 files changed

Lines changed: 28 additions & 10 deletions

File tree

src/agentex/lib/cli/templates/default-codex/project/acp.py.j2

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,15 @@ async def handle_task_event_send(params: SendEventParams):
192192
parent_span_id=turn_span.id if turn_span else None,
193193
)
194194

195-
result = await emitter.auto_send_turn(turn)
196-
197-
await process.wait()
195+
# Guarantee the subprocess is reaped even if auto_send_turn raises
196+
# (e.g. a Redis error); otherwise codex stays blocked writing to a full
197+
# stdout pipe buffer and the OS process leaks until the server restarts.
198+
try:
199+
result = await emitter.auto_send_turn(turn)
200+
finally:
201+
if process.returncode is None:
202+
process.kill()
203+
await process.wait()
198204

199205
# Record the real wall-clock duration AFTER streaming completes; setting
200206
# it before the stream ran would capture only subprocess spawn overhead.

src/agentex/lib/cli/templates/sync-codex/project/acp.py.j2

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,16 @@ async def handle_message_send(
156156
parent_span_id=turn_span.id if turn_span else None,
157157
)
158158

159-
async for event in emitter.yield_turn(turn):
160-
yield event
161-
162-
await process.wait()
159+
# Guarantee the subprocess is reaped even if the generator is abandoned
160+
# (client disconnect / GC) or yield_turn raises; otherwise codex stays
161+
# blocked writing to a full stdout pipe buffer and the process leaks.
162+
try:
163+
async for event in emitter.yield_turn(turn):
164+
yield event
165+
finally:
166+
if process.returncode is None:
167+
process.kill()
168+
await process.wait()
163169

164170
# Record the real wall-clock duration AFTER streaming completes; setting
165171
# it before the stream ran would capture only subprocess spawn overhead.

src/agentex/lib/cli/templates/temporal-codex/project/activities.py.j2

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,15 @@ async def run_codex_turn(params: RunCodexTurnParams) -> dict[str, Any]:
134134
trace_id=params.trace_id,
135135
parent_span_id=params.parent_span_id,
136136
)
137-
result = await emitter.auto_send_turn(turn, created_at=params.created_at)
138-
139-
await process.wait()
137+
# Guarantee the subprocess is reaped even if auto_send_turn raises;
138+
# otherwise codex stays blocked writing to a full stdout pipe buffer and the
139+
# OS process leaks until the worker restarts.
140+
try:
141+
result = await emitter.auto_send_turn(turn, created_at=params.created_at)
142+
finally:
143+
if process.returncode is None:
144+
process.kill()
145+
await process.wait()
140146

141147
return RunCodexTurnResult(
142148
final_text=result.final_text,

0 commit comments

Comments
 (0)