Verified against ArkNill/llm-relay@main before filing — the parser here is
byte-identical to the copy we hit it on, so this is upstream, not a fork
divergence. Measurements below come from a downstream deployment running
codex exec --json.
Summary
_parse_codex_jsonl (src/llm_relay/orch/executor.py:293 on main) matches event shapes Codex does not emit. It finds nothing, falls through to return last_message or stdout.strip() at line 317, and returns the entire raw JSONL event stream to the caller.
Measured on a real return from this session: 1,452,919 bytes returned where 368 were wanted. The parser is a no-op that has been silently disabled since it was written.
The mismatch
The parser looks for:
if event_type in ("message", "response", "assistant"):
Codex exec --json actually emits:
{"type":"item.completed","item":{"id":"item_8","type":"agent_message","text":"..."}}
type is item.completed; the payload is nested under item. Neither the primary branch nor the elif "content" in event or "text" in event fallback matches — the fallback checks top-level keys, and text lives one level down.
So last_message stays "" on every Codex call, and line 429 returns the raw stream.
Verified against real data
Replaying the current parser over a captured 1.5 MB return:
current parser last_message len: 0
=> falls through to raw stdout: True
correct extraction len: 368
reduction: 1452919 -> 368 (99.97% smaller)
Composition of that 1.5 MB:
1408 KB n=30 item.completed/command_execution ← 97%
7 KB n=30 item.started/command_execution
3 KB n=9 item.completed/agent_message ← what the caller wants
The bulk is command_execution events carrying full stdout of every shell command Codex ran — greps, git diff, file reads.
Impact
35 cli_delegate(cli="codex") returns in one session on this host: median ~200 KB, max 1.5 MB. Every one overflowed the harness cap and landed on disk, so callers paid a read cycle to retrieve a payload they discard.
It also produced a false diagnosis that cost two rounds of prompt engineering. A commissioner concluded Codex was ignoring output-discipline instructions and strengthened the prompt twice; returns grew both times. Codex was complying — his agent_message was 368 bytes. The instruction governs what Codex says; it has no reach over what the harness streams back. The shared memory feedback_codex_artifact_only_no_context_dump still records the wrong cause.
Suggested fix
def _parse_codex_jsonl(stdout: str) -> str:
"""Extract the final agent_message from Codex's JSONL event stream."""
if not stdout.strip():
return ""
last_message = ""
for line in stdout.strip().splitlines():
line = line.strip()
if not line:
continue
try:
event = json.loads(line)
except (json.JSONDecodeError, ValueError):
continue
if not isinstance(event, dict):
continue
item = event.get("item")
if isinstance(item, dict) and item.get("type") == "agent_message":
text = item.get("text")
if text:
last_message = str(text)
# legacy shapes, retained in case older codex versions are in play
elif event.get("type") in ("message", "response", "assistant"):
content = event.get("content") or event.get("text") or event.get("message")
if content:
last_message = str(content)
return last_message or stdout.strip()
Keeping the legacy branch costs nothing and covers older codex binaries.
The or stdout.strip() fallback is the thing to think hardest about. It is why this bug was invisible: a parser that silently returns 1.5 MB looks like a verbose model, not a broken parser. Options:
- Keep it, but log a warning when it fires — the failure becomes visible without changing behavior.
- Return a short diagnostic instead of the raw stream (
"[codex: no agent_message found in N events; raw output at <path>]"), so a future shape change degrades loudly rather than silently.
I lean (2), but it's a behavior change and worth a maintainer's call. Either is better than a silent 4000x amplification.
Test worth adding
A fixture with a real captured event stream, asserting the parser returns the agent_message text and not the stream. Any test that only feeds it synthetic {"type":"message"} events will pass against the broken parser — that is likely how this shipped.
Not in scope
Whether command_execution payloads should be truncated even in the on-disk artifact. They are genuinely useful for debugging a Codex run. The fix above already keeps them out of the caller's context, which is the cost that matters.
— Chris Nighswonger (VSITS) / AI Team Lead
Summary
_parse_codex_jsonl(src/llm_relay/orch/executor.py:293onmain) matches event shapes Codex does not emit. It finds nothing, falls through toreturn last_message or stdout.strip()at line 317, and returns the entire raw JSONL event stream to the caller.Measured on a real return from this session: 1,452,919 bytes returned where 368 were wanted. The parser is a no-op that has been silently disabled since it was written.
The mismatch
The parser looks for:
Codex
exec --jsonactually emits:{"type":"item.completed","item":{"id":"item_8","type":"agent_message","text":"..."}}typeisitem.completed; the payload is nested underitem. Neither the primary branch nor theelif "content" in event or "text" in eventfallback matches — the fallback checks top-level keys, andtextlives one level down.So
last_messagestays""on every Codex call, and line 429 returns the raw stream.Verified against real data
Replaying the current parser over a captured 1.5 MB return:
Composition of that 1.5 MB:
The bulk is
command_executionevents carrying full stdout of every shell command Codex ran — greps,git diff, file reads.Impact
35
cli_delegate(cli="codex")returns in one session on this host: median ~200 KB, max 1.5 MB. Every one overflowed the harness cap and landed on disk, so callers paid a read cycle to retrieve a payload they discard.It also produced a false diagnosis that cost two rounds of prompt engineering. A commissioner concluded Codex was ignoring output-discipline instructions and strengthened the prompt twice; returns grew both times. Codex was complying — his
agent_messagewas 368 bytes. The instruction governs what Codex says; it has no reach over what the harness streams back. The shared memoryfeedback_codex_artifact_only_no_context_dumpstill records the wrong cause.Suggested fix
Keeping the legacy branch costs nothing and covers older
codexbinaries.The
or stdout.strip()fallback is the thing to think hardest about. It is why this bug was invisible: a parser that silently returns 1.5 MB looks like a verbose model, not a broken parser. Options:"[codex: no agent_message found in N events; raw output at <path>]"), so a future shape change degrades loudly rather than silently.I lean (2), but it's a behavior change and worth a maintainer's call. Either is better than a silent 4000x amplification.
Test worth adding
A fixture with a real captured event stream, asserting the parser returns the
agent_messagetext and not the stream. Any test that only feeds it synthetic{"type":"message"}events will pass against the broken parser — that is likely how this shipped.Not in scope
Whether
command_executionpayloads should be truncated even in the on-disk artifact. They are genuinely useful for debugging a Codex run. The fix above already keeps them out of the caller's context, which is the cost that matters.— Chris Nighswonger (VSITS) / AI Team Lead