app-server: JSON-RPC `-32600` is overloaded across "no rollout found" and "failed to load configuration" — please make the two discriminable

Open 💬 2 comments Opened Jun 16, 2026 by HaraldSirius

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:

  1. Stale resume — recoverable. thread/resume (or a read on) a thread whose rollout no longer exists returns -32600 with a message like no rollout found / thread not found / not loaded. The correct client behavior is to fall back to thread/start (a fresh thread).
  2. 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.data discriminator, 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 a thread/resumeFailed {thread_id, reason} notification (covers Case 1; cites -32600 literally).
  • #16872 — app-server WebSocket: turns complete but rollout never materializes → sibling thread/resume fails "no rollout found" (Case 1, pure app-server).
  • #19476 — failed to load configuration: config.toml:…: invalid type (Case 2 symptom; does not note the -32600 framing 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.

View original on GitHub ↗

2 Comments

MakenRosa · 2 months ago

I can reproduce the concern from the code shape: -32600 is currently the catch-all for several app-domain failures, not just malformed JSON-RPC requests.

A few concrete spots:

  • app-server/src/error_code.rs defines invalid_request(...) as JSON-RPC -32600 with no structured data by default.
  • app-server/src/request_processors/config_errors.rs maps configuration load failures through invalid_request(format!("failed to load configuration: {err}")). For cloud config bundle failures it adds useful data (reason, errorCode, action, statusCode, detail), but the top-level JSON-RPC code is still -32600.
  • app-server/src/request_processors/thread_processor.rs maps ThreadStoreError::ThreadNotFound during resume/read to invalid_request(format!("no rollout found for thread id {thread_id}")).
  • The tests assert this behavior explicitly in several places, e.g. malformed thread/list parent ids assert -32600, while config-load and no-rollout cases assert only message contents or also flow through the same invalid_request helper.

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 via error.data.

A low-risk direction would be to keep JSON-RPC compatibility but standardize app-level subcodes in error.data, for example:

{
  "appErrorCode": "thread_not_found",
  "threadId": "..."
}

and, for config load failures, something like:

{
  "appErrorCode": "config_load_failed",
  "reason": "cloudConfigBundle",
  "errorCode": "Auth",
  "action": "relogin",
  "statusCode": 401,
  "detail": "..."
}

Then clients can stop parsing message while existing JSON-RPC clients still see the same top-level code. 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 -32600 assertions across app-server v2 methods.

HaraldSirius · 2 months ago

Thanks — the error.data.appErrorCode direction 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/start fallback, and on "failed to load configuration" to surface a "fix your project-local .codex/config.toml" message. Stable thread_not_found (with threadId) and config_load_failed subcodes would let us drop the substring matching entirely. Keeping the top-level -32600 for 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.