app-server: JSON-RPC `-32600` is overloaded across "no rollout found" and "failed to load configuration" — please make the two discriminable
Summary
Over the codex app-server JSON-RPC transport, error code -32600 is returned for at least two completely unrelated failure conditions, and the only way to tell them apart is to substring-match the human-readable message. An external client that wants to react correctly (auto-recover vs. surface to the user) has to parse English strings, which is brittle.
The two meanings we hit:
- Stale resume — recoverable.
thread/resume(or a read on) a thread whose rollout no longer exists returns-32600with a message likeno rollout found/thread not found/not loaded. The correct client behavior is to fall back tothread/start(a fresh thread). - Malformed project config — user must fix. A bad project-local
.codex/config.toml(e.g. an invalid type / unknown variant) surfaces as-32600"failed to load configuration: …config.toml:LINE:COL: …". The correct client behavior is the opposite: do not retry/fallback; show the file path + parse error to the user.
Because both arrive as -32600, a client that auto-falls-back on -32600 will silently paper over a broken config file, and a client that surfaces -32600 to the user will nag them about recoverable stale-resume cases.
Environment
- Codex CLI 0.139.0 (Homebrew cask), macOS (Darwin arm64).
- Transport:
codex app-server --listen stdio://, newline-delimited JSON-RPC 2.0. - Third-party app-server client (not the TUI).
Observed error payloads
Case 1 (stale resume):
{"id":N,"error":{"code":-32600,"message":"thread/read failed: thread not loaded: <uuid>"}}
(also seen as no rollout found for thread id <uuid>)
Case 2 (bad config):
{"id":N,"error":{"code":-32600,"message":"failed to load configuration: /path/.codex/config.toml:228:1: invalid type: ..."}}
Request
Make the two discriminable without string parsing, e.g. any of:
- Distinct codes (a dedicated code for config-load failure, and/or for thread-not-found), or
- A structured
error.datadiscriminator, e.g.{ "data": { "kind": "no_rollout" | "config_load" | ... } }, or - For the resume half specifically, the
thread/resumeFailed { thread_id, reason }notification already proposed in #22064.
A machine-readable discriminator on error.data would be the lowest-risk fix and would cover both halves at once.
Related
- #22064 —
codex exec resume <missing-uuid>: noisy "thread not found"; proposes thread/read→thread/start fallback and athread/resumeFailed {thread_id, reason}notification (covers Case 1; cites-32600literally). - #16872 — app-server WebSocket: turns complete but rollout never materializes → sibling
thread/resumefails "no rollout found" (Case 1, pure app-server). - #19476 —
failed to load configuration: config.toml:…: invalid type(Case 2 symptom; does not note the-32600framing or the collision). - PR #6938 (merged) — introduced richer v2 app-server error codes/events; the infrastructure exists but isn't applied to these two cases.
Note: the resume-half symptom (#22064/#16872) and the config-parse symptom (#19476) are each reported separately, but the overloading of -32600 across both — and the resulting inability to disambiguate from the payload — does not appear to be filed anywhere. That's the specific ask here.
2 Comments
I can reproduce the concern from the code shape:
-32600is currently the catch-all for several app-domain failures, not just malformed JSON-RPC requests.A few concrete spots:
app-server/src/error_code.rsdefinesinvalid_request(...)as JSON-RPC-32600with no structured data by default.app-server/src/request_processors/config_errors.rsmaps configuration load failures throughinvalid_request(format!("failed to load configuration: {err}")). For cloud config bundle failures it adds usefuldata(reason,errorCode,action,statusCode,detail), but the top-level JSON-RPC code is still-32600.app-server/src/request_processors/thread_processor.rsmapsThreadStoreError::ThreadNotFoundduring resume/read toinvalid_request(format!("no rollout found for thread id {thread_id}")).thread/listparent ids assert-32600, while config-load and no-rollout cases assert only message contents or also flow through the sameinvalid_requesthelper.That means clients currently have to branch on message text for cases like
no rollout found for thread id ..., while config failures are only partially structured viaerror.data.A low-risk direction would be to keep JSON-RPC compatibility but standardize app-level subcodes in
error.data, for example:and, for config load failures, something like:
Then clients can stop parsing
messagewhile existing JSON-RPC clients still see the same top-levelcode. If changing the top-level JSON-RPC code is desired later, I would do that as a second step because the current test suite has many direct-32600assertions across app-server v2 methods.Thanks — the
error.data.appErrorCodedirection is exactly what we need downstream. We're a JSON-RPC client (CodeMantis) currently forced to string-match on"no rollout found for thread id"to drive a resume→thread/startfallback, and on"failed to load configuration"to surface a "fix your project-local.codex/config.toml" message. Stablethread_not_found(withthreadId) andconfig_load_failedsubcodes would let us drop the substring matching entirely. Keeping the top-level-32600for back-compat is right for us too — we'd version-gate on the new field and keep the substring path as a fallback for older builds. Happy to validate against a branch.