app-server: first turn output blocked ~15-33s by cold model-catalog refresh when no explicit model is set

Open 💬 1 comment Opened Jul 12, 2026 by Bohan-J

What version of Codex CLI is running?

codex-cli 0.144.1 (also reproduced/reported on 0.143.0)

What subscription do you have?

ChatGPT (auth_mode="chatgpt")

Which model were you using?

Reproduces on the default-model path (no explicit model passed to the app-server). The bug is precisely about the case where the model must be resolved from the catalog; passing an explicit model avoids it (see below).

What platform is your computer?

macOS (Darwin, arm64). A downstream integrator additionally reports it on Linux x86_64.

What issue are you seeing?

Through codex app-server, starting a turn without an explicit model makes the cold model-catalog refresh sit on the critical path and block the first turn output for ~15–33s. turn/started is emitted immediately, but the first item/* event does not arrive until the refresh child times out, and stderr shows:

ERROR codex_models_manager::manager: failed to refresh available models: timeout waiting for child process to exit

This is the same underlying failure as #23119, but the impact reported there is stderr noise ("the run itself completes normally"). The point of this issue is a different, more severe impact for app-server host integrations: while the refresh is blocking, the host receives no item/* output at all. In more restricted environments (no outbound network, or the refresh child process hangs longer than it does on a healthy dev box), the block can outlast the host's own turn timeout, so the integration observes: app-server starts, turn/started fires, then zero output until the task is cancelled — i.e. an apparent "agent produced nothing" with output_bytes=0.

A real downstream report of exactly this (Codex-backed agent runtime, no output on 0.143/0.144, works again after rolling back to 0.125.0): https://github.com/multica-ai/multica/issues/5273

Controlled measurements on 0.144.1 (macOS arm64, ChatGPT auth, healthy network), same one-word prompt, driving the app-server directly:

| condition | turn/started → first item/* | failed to refresh available models on stderr | turn total |
|---|---|---|---|
| cold cache ($CODEX_HOME/models_cache.json absent), no explicit model | +32.7s | yes (1×) | 35.1s |
| warm cache (<300s old), no explicit model | +14.6s (just model latency) | none | 17.3s |
| cold cache, explicit model passed to thread/start | ~instant | none | ~2s |

So the ~15–20s of first-turn blocking is attributable to the cold-cache refresh, and it only lands on the critical path when the default model has to be resolved from the catalog. With an explicit model the refresh is skipped entirely and the first item streams immediately.

What steps can reproduce the bug?

Fresh (cold-cache) CODEX_HOME, drive the app-server over stdio, start a turn with model: null:

SCRATCH=$(mktemp -d)
cp ~/.codex/auth.json "$SCRATCH/"
# NOTE: no `model = ...` in config -> default-model resolution path
printf 'approval_policy = "never"\nsandbox_mode = "read-only"\n' > "$SCRATCH/config.toml"

CODEX_HOME="$SCRATCH" codex app-server --listen stdio://

Then send (one JSON object per line):

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"repro","title":null,"version":"0.0.1"},"capabilities":{"experimentalApi":true}}}
{"jsonrpc":"2.0","method":"initialized"}
{"jsonrpc":"2.0","id":2,"method":"thread/start","params":{"model":null,"cwd":"/tmp"}}
{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"threadId":"<thread.id from id:2 result>","input":[{"type":"text","text":"Reply with the single word: pong"}]}}

Observe: turn/started returns immediately, but the first item/* notification is delayed ~15–33s, and stderr logs the failed to refresh available models: timeout waiting for child process to exit line during the gap. Re-running within 300s (warm models_cache.json) removes the delay; passing an explicit "model" in thread/start also removes it.

  • The trigger is a cold models cache: fresh $CODEX_HOME, models_cache.json absent, or its 300s TTL elapsed (matches #23119). For integrators this means every daemon restart / fresh session / >5-min-idle turn can hit it.

What is the expected behavior?

The cold model-catalog refresh should not sit on the critical path of the first turn's output. Concretely, any of:

  1. Resolve the default model from the last-known/cached catalog immediately and refresh in the background, so turn/start can begin producing item/* output without waiting on the refresh child.
  2. Bound the refresh child with a short timeout and fall back immediately (don't let the turn's first token wait on a child that is timing out).
  3. At minimum, make the default-model path as fast as the explicit-model path (which already skips the refresh).

The stderr-noise angle (log level, retry backoff) is tracked in #23119; this issue is specifically about the first-turn output being blocked for app-server integrators when no explicit model is set.

Additional information

  • Related: #23119 (same codex_models_manager refresh timeout, framed as stderr noise / cache-TTL mechanism) and #22205 (same module, Azure custom-provider decode error — different root cause).
  • Workarounds that work today: pass an explicit model to thread/start, or keep the models cache warm.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗