Summary
A task turn that fails server-side and terminates normally leaves no reason anywhere in
status. The job is correctly marked failed, but the only diagnostic the report carries is
— a bare opening brace: the first line of the pretty-printed JSON error body. The real reason
is one command away in result <job-id>, but nothing in the status output says so, and {
reads as a rendering glitch rather than as an error.
This is distinct from #698. There the error notification is non-terminal and the job
wedges at status: running forever. Here the turn does complete: the job reaches
status: failed with phase: failed, pid: null and completedAt set. The failure is
legible in the job log and in result — everywhere except the command a caller runs first.
Fixing #698 would not fix this. Making the error notification terminal routes the turn into
the same non-throwing completion path described below, where the summary is still { and
errorMessage is still never written.
Root cause — two independent gaps that meet
1. The summary is the first line of the raw output.
plugins/codex/scripts/codex-companion.mjs:525:
summary: firstMeaningfulLine(rawOutput, firstMeaningfulLine(failureMessage, `${taskMetadata.title} finished.`)),
firstMeaningfulLine (:174) returns the first non-blank trimmed line. On a failed turn
rawOutput is the pretty-printed error body, so that line is the opening {. Note that
shorten() (lib/codex.mjs:90) does collapse whitespace, but it is not on this path.
2. errorMessage is only written on the throwing path.
plugins/codex/scripts/lib/tracked-jobs.mjs writes errorMessage only inside the catch
(:182, :189, :199). A turn that fails server-side but completes returns normally, so the
success branch runs (:174, summary: execution.summary) and no errorMessage is ever
stored.
renderJobStatusReport already has the branch that would print it —
lib/render.mjs:437-440 — and on this path it is dead, falling through to
"No captured result payload was stored for this job."
Reproduction
Any 400 from the turn reproduces this; the trigger below is incidental. I used an effort value
the current model rejects (that vocabulary drift is #751 — not what this issue is about).
node scripts/codex-companion.mjs task --background --fresh --effort minimal "Reply with exactly: OK"
status — what a caller sees first
# Codex Job Status
- task-mtyt7j74-roqc7q | failed | rescue | Codex Task
Summary: {
Phase: failed
Duration: 3s
...
Progress:
Thread ready (...).
Turn started (...).
Codex error: {
Turn failed.
Both the Summary: field and the Codex error: progress line are cut to {.
The stored index row
{"id":"task-mtyt7j74-roqc7q","status":"failed","summary":"{"}
errorMessage is absent from both the index entry and the job file. The job file's keys are
id, kind, kindLabel, title, workspaceRoot, jobClass, summary, write, createdAt, status, phase, pid, logFile, request, startedAt, threadId, turnId, completedAt, result, rendered — no
errorMessage.
result — where the reason actually is
{
"type": "error",
"error": {
"type": "invalid_request_error",
"code": "unsupported_value",
"message": "Unsupported value: 'minimal' is not supported with the 'gpt-5.6-terra' model. Supported values are: 'none', 'low', 'medium', 'high', 'xhigh', and 'max'.",
"param": "reasoning.effort"
},
"status": 400
}
Complete and diagnostic — reached only by knowing to ask for it.
Why this is worth more than a cosmetic fix
The failure is a 400 on the first turn, about four seconds in, after the thread has already
reported ready. An agent that delegates to Codex, sees failed, and does not go on to open
result or the log concludes that Codex is broken or unavailable on that machine. That is the
wrong conclusion, and it is the one the output steers a reader toward: a summary of { looks
like the tool is malfunctioning, not like the request was rejected for a stated reason.
It costs a person the same way. status is the documented next step after a background task,
and it is the one view that currently cannot say what went wrong.
Suggested fix
Either half alone would make the failure legible; both are small.
- Set
errorMessage on the non-throwing failure path, from result.error.message when the
turn completed with a failed status. The renderer already prints it.
- Make the summary robust to a structured body: prefer
error.message when the payload parses
as an error, and otherwise put firstMeaningfulLine through the same whitespace collapsing
shorten() already does, so a multi-line value can never reduce to punctuation.
The same { truncation affects the Codex error: progress line, which comes from
lib/codex.mjs:539 where message.params.error.message is interpolated raw and not shortened.
Environment
- Plugin
codex v1.0.5 as installed; every line cited re-checked against main @ v1.0.6 and
unchanged, at identical line numbers
codex-cli 0.153.4
- Windows 11 (26200), Node v24.19.0, ChatGPT sign-in
- Model
gpt-5.6-terra (pinned in ~/.codex/config.toml by the Codex desktop app)
Related: #698 (the non-terminating variant of a Codex-side failure), #695 (failure reason
discarded on the stop-gate paths), #751 (the effort vocabulary drift that made a convenient
trigger here).
Summary
A
taskturn that fails server-side and terminates normally leaves no reason anywhere instatus. The job is correctly markedfailed, but the only diagnostic the report carries is— a bare opening brace: the first line of the pretty-printed JSON error body. The real reason
is one command away in
result <job-id>, but nothing in thestatusoutput says so, and{reads as a rendering glitch rather than as an error.
This is distinct from #698. There the
errornotification is non-terminal and the jobwedges at
status: runningforever. Here the turn does complete: the job reachesstatus: failedwithphase: failed,pid: nullandcompletedAtset. The failure islegible in the job log and in
result— everywhere except the command a caller runs first.Fixing #698 would not fix this. Making the
errornotification terminal routes the turn intothe same non-throwing completion path described below, where the summary is still
{anderrorMessageis still never written.Root cause — two independent gaps that meet
1. The summary is the first line of the raw output.
plugins/codex/scripts/codex-companion.mjs:525:firstMeaningfulLine(:174) returns the first non-blank trimmed line. On a failed turnrawOutputis the pretty-printed error body, so that line is the opening{. Note thatshorten()(lib/codex.mjs:90) does collapse whitespace, but it is not on this path.2.
errorMessageis only written on the throwing path.plugins/codex/scripts/lib/tracked-jobs.mjswriteserrorMessageonly inside thecatch(
:182,:189,:199). A turn that fails server-side but completes returns normally, so thesuccess branch runs (
:174,summary: execution.summary) and noerrorMessageis everstored.
renderJobStatusReportalready has the branch that would print it —lib/render.mjs:437-440— and on this path it is dead, falling through to"No captured result payload was stored for this job."Reproduction
Any 400 from the turn reproduces this; the trigger below is incidental. I used an effort value
the current model rejects (that vocabulary drift is #751 — not what this issue is about).
status— what a caller sees firstBoth the
Summary:field and theCodex error:progress line are cut to{.The stored index row
{"id":"task-mtyt7j74-roqc7q","status":"failed","summary":"{"}errorMessageis absent from both the index entry and the job file. The job file's keys areid, kind, kindLabel, title, workspaceRoot, jobClass, summary, write, createdAt, status, phase, pid, logFile, request, startedAt, threadId, turnId, completedAt, result, rendered— noerrorMessage.result— where the reason actually isComplete and diagnostic — reached only by knowing to ask for it.
Why this is worth more than a cosmetic fix
The failure is a 400 on the first turn, about four seconds in, after the thread has already
reported ready. An agent that delegates to Codex, sees
failed, and does not go on to openresultor the log concludes that Codex is broken or unavailable on that machine. That is thewrong conclusion, and it is the one the output steers a reader toward: a summary of
{lookslike the tool is malfunctioning, not like the request was rejected for a stated reason.
It costs a person the same way.
statusis the documented next step after a background task,and it is the one view that currently cannot say what went wrong.
Suggested fix
Either half alone would make the failure legible; both are small.
errorMessageon the non-throwing failure path, fromresult.error.messagewhen theturn completed with a failed status. The renderer already prints it.
error.messagewhen the payload parsesas an error, and otherwise put
firstMeaningfulLinethrough the same whitespace collapsingshorten()already does, so a multi-line value can never reduce to punctuation.The same
{truncation affects theCodex error:progress line, which comes fromlib/codex.mjs:539wheremessage.params.error.messageis interpolated raw and not shortened.Environment
codexv1.0.5 as installed; every line cited re-checked againstmain@ v1.0.6 andunchanged, at identical line numbers
codex-cli0.153.4gpt-5.6-terra(pinned in~/.codex/config.tomlby the Codex desktop app)Related: #698 (the non-terminating variant of a Codex-side failure), #695 (failure reason
discarded on the stop-gate paths), #751 (the effort vocabulary drift that made a convenient
trigger here).