Skip to content

Commit 42aa037

Browse files
declan-scaleclaude
andcommitted
fix(codex): drain-free stderr + UTF-8-safe stdout decode in tutorials [greptile]
Greptile flagged a deadlock: all three codex tutorials spawned `codex exec` with stderr=PIPE but never read it, so once codex filled the OS pipe buffer (~64 KB) the subprocess stalled and the agent hung. Redirect stderr to DEVNULL (codex --json emits events on stdout; stderr is progress noise). Also fix UTF-8 corruption in _process_stdout: decoding each 4 KB read independently splits multibyte characters at chunk boundaries. Use an incremental UTF-8 decoder so boundary-split characters decode correctly. Applied to 00_sync, 10_async/00_base (acp.py) and 10_async/10_temporal (activities.py). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c37646c commit 42aa037

3 files changed

Lines changed: 39 additions & 9 deletions

File tree

  • examples/tutorials

examples/tutorials/00_sync/harness_codex/project/acp.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import os
2424
import time
25+
import codecs
2526
import asyncio
2627
from typing import AsyncGenerator
2728
from collections.abc import AsyncIterator
@@ -87,25 +88,34 @@ async def _spawn_codex(model: str) -> asyncio.subprocess.Process:
8788
*cmd,
8889
stdin=asyncio.subprocess.PIPE,
8990
stdout=asyncio.subprocess.PIPE,
90-
stderr=asyncio.subprocess.PIPE,
91+
# Discard stderr: codex --json writes events to stdout; its stderr is
92+
# progress/debug noise. Capturing it with PIPE but never reading it
93+
# would deadlock once codex fills the OS pipe buffer (~64 KB).
94+
stderr=asyncio.subprocess.DEVNULL,
9195
env={**os.environ},
9296
)
9397

9498

9599
async def _process_stdout(process: asyncio.subprocess.Process) -> AsyncIterator[str]:
96-
"""Yield newline-delimited JSON lines from the process stdout."""
100+
"""Yield newline-delimited JSON lines from the process stdout.
101+
102+
Uses an incremental UTF-8 decoder so a multibyte character split across two
103+
4 KB reads is decoded correctly instead of being corrupted at the boundary.
104+
"""
97105
assert process.stdout is not None
106+
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
98107
buffer = ""
99108
while True:
100109
chunk = await process.stdout.read(4096)
101110
if not chunk:
102111
break
103-
buffer += chunk.decode("utf-8", errors="replace")
112+
buffer += decoder.decode(chunk)
104113
while "\n" in buffer:
105114
line, buffer = buffer.split("\n", 1)
106115
line = line.strip()
107116
if line:
108117
yield line
118+
buffer += decoder.decode(b"", final=True)
109119
if buffer.strip():
110120
yield buffer.strip()
111121

examples/tutorials/10_async/00_base/harness_codex/project/acp.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import os
2424
import time
25+
import codecs
2526
import asyncio
2627
from collections.abc import AsyncIterator
2728

@@ -102,25 +103,34 @@ async def _spawn_codex(
102103
*cmd,
103104
stdin=asyncio.subprocess.PIPE,
104105
stdout=asyncio.subprocess.PIPE,
105-
stderr=asyncio.subprocess.PIPE,
106+
# Discard stderr: codex --json writes events to stdout; its stderr is
107+
# progress/debug noise. Capturing it with PIPE but never reading it
108+
# would deadlock once codex fills the OS pipe buffer (~64 KB).
109+
stderr=asyncio.subprocess.DEVNULL,
106110
env={**os.environ},
107111
)
108112

109113

110114
async def _process_stdout(process: asyncio.subprocess.Process) -> AsyncIterator[str]:
111-
"""Yield newline-delimited JSON lines from the process stdout."""
115+
"""Yield newline-delimited JSON lines from the process stdout.
116+
117+
Uses an incremental UTF-8 decoder so a multibyte character split across two
118+
4 KB reads is decoded correctly instead of being corrupted at the boundary.
119+
"""
112120
assert process.stdout is not None
121+
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
113122
buffer = ""
114123
while True:
115124
chunk = await process.stdout.read(4096)
116125
if not chunk:
117126
break
118-
buffer += chunk.decode("utf-8", errors="replace")
127+
buffer += decoder.decode(chunk)
119128
while "\n" in buffer:
120129
line, buffer = buffer.split("\n", 1)
121130
line = line.strip()
122131
if line:
123132
yield line
133+
buffer += decoder.decode(b"", final=True)
124134
if buffer.strip():
125135
yield buffer.strip()
126136

examples/tutorials/10_async/10_temporal/harness_codex/project/activities.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from __future__ import annotations
1717

1818
import os
19+
import codecs
1920
import asyncio
2021
from typing import Any
2122
from datetime import datetime
@@ -82,25 +83,34 @@ async def _spawn_codex(
8283
*cmd,
8384
stdin=asyncio.subprocess.PIPE,
8485
stdout=asyncio.subprocess.PIPE,
85-
stderr=asyncio.subprocess.PIPE,
86+
# Discard stderr: codex --json writes events to stdout; its stderr is
87+
# progress/debug noise. Capturing it with PIPE but never reading it
88+
# would deadlock once codex fills the OS pipe buffer (~64 KB).
89+
stderr=asyncio.subprocess.DEVNULL,
8690
env={**os.environ},
8791
)
8892

8993

9094
async def _process_stdout(process: asyncio.subprocess.Process) -> AsyncIterator[str]:
91-
"""Yield newline-delimited JSON lines from the process stdout."""
95+
"""Yield newline-delimited JSON lines from the process stdout.
96+
97+
Uses an incremental UTF-8 decoder so a multibyte character split across two
98+
4 KB reads is decoded correctly instead of being corrupted at the boundary.
99+
"""
92100
assert process.stdout is not None
101+
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
93102
buffer = ""
94103
while True:
95104
chunk = await process.stdout.read(4096)
96105
if not chunk:
97106
break
98-
buffer += chunk.decode("utf-8", errors="replace")
107+
buffer += decoder.decode(chunk)
99108
while "\n" in buffer:
100109
line, buffer = buffer.split("\n", 1)
101110
line = line.strip()
102111
if line:
103112
yield line
113+
buffer += decoder.decode(b"", final=True)
104114
if buffer.strip():
105115
yield buffer.strip()
106116

0 commit comments

Comments
 (0)