Skip to content

fix(runtime): unwrap codex exec transcript in Start() Raw + cap synth judge answers (#724)#726

Merged
jerryfane merged 1 commit into
mainfrom
fix/724-codex-unwrap-prompt-caps
Jul 7, 2026
Merged

fix(runtime): unwrap codex exec transcript in Start() Raw + cap synth judge answers (#724)#726
jerryfane merged 1 commit into
mainfrom
fix/724-codex-unwrap-prompt-caps

Conversation

@jerryfane

Copy link
Copy Markdown
Owner

What

Two composing halves of the same live skillopt synth failure (2026-07-08), the codex sibling of #722.

(1) codex Start().Raw unwrap. CodexAdapter.Start runs codex exec --json and returned StartResult{Raw: result.Stdout} verbatim — the full JSONL transcript (thread.started banner, turn.started, reasoning items, agent_message, turn.completed usage), not the assistant's answer. The sole consumer of Start().Raw is realSkillOptABDeliver's forked-session path (empty RuntimeRef → Start), which feeds skillopt synth (challenger + judge), ab, and the live-AB challenger. Those consumers parse Raw as the assistant's answer, so they saw the whole transcript and could not find the challenger item's context/question/rubric. This is the exact codex flavor of #722's claude envelope leak.

(2) synth judge-prompt hygiene. synthJudgePrompt embedded both weak/strong answers verbatim with no cap. Even after (1) unwraps the answer, a genuinely long answer combined with both halves embedded blew ARG_MAX on the kimi judge exec (issue evidence: run 2, items 1 & 3 died at the judge exec).

Why

The judge scores answer quality against a rubric — it does not need a full runtime transcript. And a forked-session consumer that parses Raw as the assistant's answer must receive the answer, not the CLI's event stream.

How

  • internal/runtime/adapter.go CodexAdapter.Start: after parsing the thread id, reuse parseCodexJSONResult (the same parser the Deliver path uses to build Summary) to surface the joined agent_message text as Raw. Fail-open: fall back to the raw stdout when the stream carries no agent_message (older CLI, plain-text fallback, unexpected shape), so Raw always surfaces the underlying text and unwrap never errors. RuntimeRef is untouched. Same altitude/pattern as fix(runtime): unwrap claude CLI JSON envelope in Start() Raw #722 (b53269f).
  • internal/cli/skillopt_synth.go: new capSynthAnswer caps each embedded answer to synthMaxAnswerBytes (12KB) with a byte-accurate [truncated N bytes] marker; synthJudgePrompt wraps both weak and strong answers. Under-limit answers are embedded byte-identical (no marker).

Additive and fail-open; no CLI surface change (internal adapter/prompt behavior, like #722 which shipped without docs changes).

How tested

Toolchain pinned: export PATH=/root/.local/toolchains/go1.26.4/bin:$PATH, isolated HERDR_SOCKET_PATH, HERDR_ENV unset.

  • go build ./... → exit 0
  • go vet ./internal/runtime ./internal/cli → exit 0
  • go test -count=1 ./internal/runtime → ok (0.081s)
  • go test -race -count=1 ./internal/runtime → ok (1.122s)
  • go test -count=1 ./internal/cli → ok (224.832s)

New tests (all PASS):

  • TestCodexStartUnwrapsJSONTranscript — adapter unit: unwraps agent_message text, joins multiple messages, falls back to full stdout when no agent_message; RuntimeRef preserved in every case. Uses a real codex exec --json transcript shape.
  • TestRealSkillOptABDeliverUnwrapsCodexTranscriptForSynth — consumer regression through the real realSkillOptABDeliver seam with a real CodexAdapter over a fake runner (codex flavor of fix(runtime): unwrap claude CLI JSON envelope in Start() Raw #722's TestRealSkillOptABDeliverUnwrapsClaudeEnvelopeForSynth); the delivered answer must be the agent_message and parseSynthGeneratedItem must succeed.
  • TestSynthJudgePromptCapsAnswers — cap + byte-accurate marker for oversized answers, verbatim under-limit, exact-at-limit not truncated.

Closes #724

🤖 Generated with Claude Code

… judge answers (#724)

Two composing halves of the same live `skillopt synth` failure (2026-07-08):

1. codex Start().Raw was the full `codex exec --json` transcript (thread.started
   banner, turn events, reasoning items), not the assistant's answer — the codex
   flavor of the claude bug fixed in #722 (b53269f). Forked-session consumers
   (skillopt synth/ab) that parse Start().Raw as the assistant's answer saw the
   whole transcript and could not find the challenger item's context/question/
   rubric. Fix at the adapter (same altitude as #722): reuse parseCodexJSONResult
   — the exact parser the Deliver path uses to build Summary — to surface the
   joined agent_message text as Raw, failing open to the raw stdout when the
   stream carries no agent_message (older CLI, plain-text fallback, unexpected
   shape). RuntimeRef is untouched.

2. synthJudgePrompt embedded both weak/strong answers verbatim with no cap. Even
   after the unwrap a genuinely long answer, combined with both halves embedded,
   blew ARG_MAX on the kimi judge exec. capSynthAnswer now truncates each embedded
   answer to synthMaxAnswerBytes (12KB) with a byte-accurate "[truncated N bytes]"
   marker, so judge prompts stay small regardless of runtime verbosity. Under-limit
   answers are embedded byte-identical with no marker.

Tests: TestCodexStartUnwrapsJSONTranscript (adapter unit: agent_message text vs
transcript fallback, RuntimeRef preserved); TestRealSkillOptABDeliverUnwrapsCodex-
TranscriptForSynth (consumer regression through the real delivery seam, codex
flavor of #722's claude test); TestSynthJudgePromptCapsAnswers (cap + marker +
verbatim-under-limit + exact-at-limit).

Closes #724

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jerryfane jerryfane merged commit 37684fc into main Jul 7, 2026
1 check passed
@jerryfane jerryfane deleted the fix/724-codex-unwrap-prompt-caps branch July 7, 2026 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

synth: embedded weak/strong answers are raw runtime stdout (codex banner/transcript bloat) — unwrap + cap before building judge prompts

1 participant