Summary
CodexCliBackend passes the prompt as a positional argv element (cmd += ["--", prompt]). On Windows, resolve_codex_path() resolves to the npm shim codex.CMD, so subprocess.run executes it through cmd.exe — and cmd.exe ends the command line at the first CR/LF. Every prompt the sleep engine builds is multi-line, so codex only ever receives line 1 and the rest is silently dropped.
Line 1 of the attempt prompt is a bare wrapper with no task in it:
Complete the following task for the user. Follow the skill and memory guidance below, including any output-format and length requirements. …
So the model replies What task would you like me to complete? for every task. The judge scores that 0.0, baseline and candidate both come out 0.000, and the gate rejects. The night looks like "nothing to learn" instead of "the backend never delivered the prompt".
This is the codex-side twin of #121 — same npm .cmd shim, same silent 0-score failure mode. #121 fixed path resolution for ClaudeCliBackend; CodexCliBackend's argv delivery was left as-is. ClaudeCliBackend._call already pipes over stdin and even documents why (backend.py, "cmd.exe whose command line caps at ~8K chars … claude -p with no positional prompt reads stdin") — codex never got the same treatment.
Still present on main @ 8a4c96a.
Impact
Any SkillOpt-Sleep run with --backend codex on Windows. It fails silently and in the worst possible direction:
-
Every rollout scores 0.0, so the gate can never accept — the run always reports reject.
-
Nothing surfaces in last_call_error: exit code is 0, the output file is non-empty, the response is a well-formed English sentence. diagnostics.json shows call_error: "".
-
The reflect stage then reads a transcript full of "please give me the task" and mines rules for a defect that does not exist. My night-1 report proposed 8 edits, all variants of:
用户消息含有明确问题、动作、链接或目标时,立即视为可执行任务;禁止回复"请提供任务、要求、文件或预期结果"等通用索要语
(When the user's message already contains a question, action, link or goal, treat it as actionable immediately; never reply with generic requests for "the task, requirements, files or expected result".)
Those would have been written into SKILL.md and memory as hard rules if the gate had accepted them. A gate failure was the only thing preventing corruption of the skill being optimized.
Reproduction
36 characters, one newline, no SkillOpt needed:
import subprocess, tempfile, os
codex = r"D:\path\to\codex.CMD" # the npm shim, not the .exe
out = tempfile.NamedTemporaryFile(prefix="p_", suffix=".txt", delete=False).name
prompt = "Ignore everything except the next line.\nAnswer in one word: what is the capital of China?"
subprocess.run([codex, "exec", "--skip-git-repo-check", "--color", "never",
"--sandbox", "read-only", "-o", out, "--", prompt],
capture_output=True, text=True)
print(open(out, encoding="utf-8").read())
Observed: OK. — line 2 never arrived.
Expected: Beijing.
Passing the same string over stdin (cmd + ["-"], input=prompt) returns Beijing.
Isolating the trigger
Four cases against the same codex install, and the 1147-char / 37-newline attempt prompt taken verbatim from a real run's evidence.jsonl:
| # |
prompt |
chars |
newlines |
channel |
result |
| A |
real attempt prompt |
1147 |
37 |
argv |
❌ What task would you like me to complete? |
| B |
same string |
1147 |
37 |
stdin |
✅ correct, task-specific answer |
| C |
short, single line |
23 |
0 |
argv |
✅ correct |
| D |
short, one newline |
36 |
1 |
argv |
❌ only line 1 answered |
A vs B: identical bytes, only the channel differs. C vs D: 36 characters is enough to break it. So it is not prompt length, not the ~8K cmd.exe cap, and not encoding — it is the newline.
Affected sites
Both on main @ 8a4c96a:
skillopt_sleep/backend.py:961 — CodexCliBackend._call_once (attempt / judge / reflect)
skillopt_sleep/backend.py:1104 — CodexCliBackend.attempt_with_tools (tool rollout, same multi-line prompt shape)
Suggested fix
Mirror what ClaudeCliBackend already does — send the prompt over stdin. codex exec - reads it from there:
- cmd += ["--", prompt]
+ cmd += ["-"]
proc = None
try:
try:
proc = subprocess.run(
cmd,
+ input=prompt,
capture_output=True,
creationflags=_NO_WINDOW,
text=True,
+ encoding="utf-8",
+ errors="replace",
timeout=self.timeout,
cwd=self.project_dir or None,
)
The encoding="utf-8" is a separate latent bug worth folding in: text=True without an explicit encoding uses the locale codepage, which is GBK on a zh-CN Windows install and raises UnicodeDecodeError on non-ASCII codex output.
Verified locally through the real CodexCliBackend._call_once() with case A's prompt: response goes from 58 chars of boilerplate to a 591-char task-specific answer, last_call_error empty.
Happy to send a PR if the approach looks right.
Environment
- SkillOpt
main @ 8a4c96a (also reproduced on 374c832)
- codex-cli 0.144.4, installed via npm (nvm-windows) →
codex.CMD
- Windows 10 Pro 19045, Python 3.13.5, zh-CN locale
Summary
CodexCliBackendpasses the prompt as a positional argv element (cmd += ["--", prompt]). On Windows,resolve_codex_path()resolves to the npm shimcodex.CMD, sosubprocess.runexecutes it throughcmd.exe— andcmd.exeends the command line at the first CR/LF. Every prompt the sleep engine builds is multi-line, so codex only ever receives line 1 and the rest is silently dropped.Line 1 of the attempt prompt is a bare wrapper with no task in it:
So the model replies
What task would you like me to complete?for every task. The judge scores that 0.0, baseline and candidate both come out 0.000, and the gate rejects. The night looks like "nothing to learn" instead of "the backend never delivered the prompt".This is the codex-side twin of #121 — same npm
.cmdshim, same silent 0-score failure mode. #121 fixed path resolution forClaudeCliBackend;CodexCliBackend's argv delivery was left as-is.ClaudeCliBackend._callalready pipes over stdin and even documents why (backend.py, "cmd.exe whose command line caps at ~8K chars …claude -pwith no positional prompt reads stdin") — codex never got the same treatment.Still present on
main@ 8a4c96a.Impact
Any SkillOpt-Sleep run with
--backend codexon Windows. It fails silently and in the worst possible direction:Every rollout scores 0.0, so the gate can never accept — the run always reports
reject.Nothing surfaces in
last_call_error: exit code is 0, the output file is non-empty, the response is a well-formed English sentence.diagnostics.jsonshowscall_error: "".The reflect stage then reads a transcript full of "please give me the task" and mines rules for a defect that does not exist. My night-1 report proposed 8 edits, all variants of:
Those would have been written into SKILL.md and memory as hard rules if the gate had accepted them. A gate failure was the only thing preventing corruption of the skill being optimized.
Reproduction
36 characters, one newline, no SkillOpt needed:
Observed:
OK.— line 2 never arrived.Expected:
Beijing.Passing the same string over stdin (
cmd + ["-"],input=prompt) returnsBeijing.Isolating the trigger
Four cases against the same codex install, and the 1147-char / 37-newline attempt prompt taken verbatim from a real run's
evidence.jsonl:What task would you like me to complete?A vs B: identical bytes, only the channel differs. C vs D: 36 characters is enough to break it. So it is not prompt length, not the ~8K
cmd.execap, and not encoding — it is the newline.Affected sites
Both on
main@ 8a4c96a:skillopt_sleep/backend.py:961—CodexCliBackend._call_once(attempt / judge / reflect)skillopt_sleep/backend.py:1104—CodexCliBackend.attempt_with_tools(tool rollout, same multi-line prompt shape)Suggested fix
Mirror what
ClaudeCliBackendalready does — send the prompt over stdin.codex exec -reads it from there:The
encoding="utf-8"is a separate latent bug worth folding in:text=Truewithout an explicit encoding uses the locale codepage, which is GBK on a zh-CN Windows install and raisesUnicodeDecodeErroron non-ASCII codex output.Verified locally through the real
CodexCliBackend._call_once()with case A's prompt: response goes from 58 chars of boilerplate to a 591-char task-specific answer,last_call_errorempty.Happy to send a PR if the approach looks right.
Environment
main@ 8a4c96a (also reproduced on 374c832)codex.CMD