Repository navigation
[Bug]: Service restart rewrites the Codex catalog without handling stale app-servers, so routed models stay missing from the App picker #1046
Description
Client or integration
Codex App
Area
Catalog / models
Summary
After the proxy rewrites the Codex model catalog on startup, routed models stay missing from the Codex App model picker until the long-lived Codex app-server process is killed by hand. The catalog file on disk is already correct at that point, so the state looks healthy from every disk-level check while the picker still shows only native models.
The stale-app-server problem itself is known and already handled for the CLI sync path (#476, #857). The gap is that the server startup path performs the same catalog/cache write without the accompanying app-server handling:
src/cli/index.ts(sync,sync-cache) callsinvalidateCodexModelsCache()and thenafterCatalogWriteHandleAppServers({ restart }), which warns about stale app-servers or SIGTERMs them.src/server/index.ts:313calls the sameinvalidateCodexModelsCache()on boot with no equivalent follow-up.afterCatalogWriteHandleAppServersis never referenced outsidesrc/cli/index.ts.
So ocx service repair / a service restart rewrites models_cache.json and leaves an already-running app-server holding the previous in-memory list, with no warning emitted. The user sees a correct catalog on disk and an incorrect picker, and nothing in the output suggests why.
This is most visible in an SSH remote workspace, where the app-server is long-lived and boots independently of the app, but it is not SSH-specific.
Note that collectCodexAppServerCatalogState() already computes exactly the signal needed here (stale when an app-server start time predates the catalog mtime). It just is not consulted on the server startup path.
Reproduction
- Install and run opencodex on a host with at least one routed provider configured (Anthropic OAuth in my case) and confirm the routed models are enabled.
- Open that host as a workspace in the Codex App so a long-lived
codex ... app-server --listen unix://process is running. - On the host, change something that causes a catalog rewrite and restart the service:
ocx service repair. - Confirm the on-disk state is correct:
~/.codex/opencodex-catalog.jsonlists the routed models withvisibility: "list"~/.codex/models_cache.jsonhas the same entriescurl "http://127.0.0.1:10100/v1/models?client_version=<codex version>"returns themcodex exec --model <provider>/<routed-model> "..."answers normally
- Open the model picker in the Codex App.
Actual: only the native gpt-* models are listed. The routed entries are absent even though every check in step 4 passes.
Expected: either the routed models appear, or the service restart emits the same stale-app-server warning the CLI sync path already produces (formatStaleCodexAppServerWarning).
Workaround that fixes it immediately:
pkill -f "app-server --listen unix://"
rm -f ~/.codex/app-server-control/app-server-control.sock
After the app-server restarts, the picker shows the routed models.
One secondary detail from the same session: the app-server left a stale app-server-control.sock behind, and the replacement process logged Error: app-server control socket is already in use on startup. It kept running, but the removal above was needed before the restart came up clean.
Version
2.10.0
Operating system
macOS 15 (Apple Silicon, Darwin 24.6.0)
Provider and model
anthropic / claude-opus-5, claude-fable-5 (routed as anthropic/claude-opus-5, anthropic/claude-fable-5)
Logs or error output
Service log after the restart, showing the catalog write with no stale-app-server warning:
🚀 opencodex proxy running on http://localhost:10100
[opencodex] Codex runtime: /Users/[USER]/.local/bin/codex (version=0.146.0, source=configured)
Pointed Codex's built-in openai provider at the opencodex proxy (openai_base_url).
Codex model catalog: /Users/[USER]/.codex/opencodex-catalog.json
[opencodex] catalog sync: routed model fetch returned empty; preserving 11 existing routed entries on disk.
Catalog and cache agree on disk while the picker disagrees:
$ python3 - <<'PY'
import json
d=json.load(open("/Users/[USER]/.codex/opencodex-catalog.json"))
for m in d["models"]: print(m["slug"], m.get("visibility"))
PY
gpt-5.6-sol list
gpt-5.6-terra list
gpt-5.6-luna list
gpt-5.5 list
gpt-5.4 list
gpt-5.4-mini list
gpt-5.3-codex-spark list
anthropic/claude-fable-5 list
anthropic/claude-opus-5 list
Routing works end to end the whole time:
$ codex exec --model anthropic/claude-fable-5 --skip-git-repo-check "reply with exactly: FABLE_OK"
FABLE_OK
Stale app-server startup error mentioned above:
Error: app-server control socket is already in use at /Users/[USER]/.codex/app-server-control/app-server-control.sock
Screenshots and supporting files
Model picker after the catalog rewrite: only 5.5, 5.6 Sol, 5.6 Terra, 5.6 Luna, 5.4 Mini, 5.4, 5.3 Codex Spark are offered; no anthropic/* entries.
Redacted configuration
{
"port": 10100,
"providers": {
"openai": {
"adapter": "openai-responses",
"baseUrl": "https://chatgpt.com/backend-api/codex",
"authMode": "forward",
"codexAccountMode": "pool"
},
"anthropic": {
"adapter": "anthropic",
"baseUrl": "https://api.anthropic.com",
"authMode": "oauth",
"defaultModel": "claude-sonnet-5"
}
},
"defaultProvider": "openai",
"codexAutoStart": true,
"websockets": false,
"disabledModels": ["anthropic/claude-sonnet-5", "..."]
}(Most Anthropic models are intentionally in disabledModels; claude-opus-5 and claude-fable-5 are the two deliberately left enabled, and those are the two missing from the picker.)
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
github-actions commentedon Aug 5, 2026
Automated translation bookkeeping — detected language: unknown.
lidge-jun commentedon Aug 5, 2026
Follow-up investigation with a controlled two-host comparison. This narrows the report above considerably, and also corrects one thing I got wrong.
The asymmetry that identifies the cause. The same Codex App, from the same client machine, opens SSH workspaces on two remote macOS hosts. Host B shows every routed model; host A shows only native
gpt-*. Both run opencodex 2.10.0, SSH-sessioncodex0.146.0, identicalopenai_base_url/model_catalog_jsoninjection, and both have the routed entries present withvisibility: "list"in the catalog and inmodels_cache.json.The only discriminator is the app-server start time relative to the catalog mtime:
Host B's app-server booted after the last catalog write, so it read the finished file. Host A's catalog was rewritten 4m27s after its app-server booted, so the app-server serves an in-memory list that no longer exists on disk. Every check we ran reads the file; the picker renders memory.
Two things that are NOT the cause, both ruled out by the comparison:
app-server-control.sockcollision I mentioned in the report — host B logs the identicalcontrol socket is already in useerror and its picker is fine.client_version: "0.0.0"/fetched_at: 2000-01-01stamp inmodels_cache.json— that is the deliberate sentinel written byinvalidateCodexModelsCache(), and both hosts carry it.Why a proxy restart triggers it. The startup log order is literal:
ocx startawaitssyncModelsToCodex(port)atsrc/cli/index.ts:319, well afterstartServer(). So a listening socket is not a signal that the catalog has settled, and every proxy start rewrites the catalog (host B's log shows+ 29/+ 32on its own restarts — this is normal, not something unusual about host A).Why SSH is where it surfaces. Locally the app spawns and supervises the app-server, so quitting and reopening the app restarts it. Over SSH the app
nohupscodex ... app-server --listen unix://on the remote host, detached (ppid 1), with an explicitpkill ... desktop-ssh-websocket-v0.sockguard — it is built to outlive a single connection, and reconnecting the workspace reuses it. So on a remote host the app-server routinely outlives both the app session and the proxy restart, and no user-visible action restarts it. The local fix ("quit and reopen") does nothing. That matches #851 being reported specifically against SSH workspaces.The defect itself is surface-independent, though: any long-lived app-server predating a proxy restart yields a stale picker.
Correction to the original report. I initially suspected
multiAgentModebeing absent from the remote config, since the two hosts genuinely differed there. That was wrong:isEligibleV2SubagentEntry()(src/codex/catalog/sync.ts:70) allows an absentmulti_agent_version, and the routed entries already hadvisibility: "list"before that config was touched. Setting it changed the catalog bytes without being load-bearing — the app-server restart that followed is what fixed the picker.Suggested fix, unchanged in substance but better scoped.
collectCodexAppServerCatalogState()already returnsstalefor exactly host A's numbers. Consult it on the startup path after a catalog/cache write and emit the existingformatStaleCodexAppServerWarning. Warning rather than terminating: a proxy start happens on login, onservice repair, and on update, and SIGTERM-ing an app-server at those moments could kill an in-flight remote turn. Termination should stay opt-in via--restart-codex.Worth considering as a second surface:
ocx statuswas fully green on host A (includingCatalog clamp: inactive) while the picker was wrong. It could report the samestalestate.Related: #476 (detection built), #857 (staleness classifier), #851 (SSH workspaces, closed on template grounds rather than resolved). Distinct from #241, where the catalog is correct and Desktop omits routed models anyway — that looks like a different, upstream failure and I would not conflate the two.
Merge pull request #1072 from lidge-jun/codex/1046-startup-stale-app-…
lidge-jun commentedon Aug 6, 2026
Fixed on dev by PR #1072, merge commit 2a0cdfc. Startup catalog writes now detect and warn about stale app-servers instead of silently leaving routed models missing from the App picker (the restart path was deliberately not auto-triggered at boot — it SIGTERMs active turns; the warning names the stale processes). Ships with the next release train.
fix(codex): warn when a startup write leaves an app-server stale (lid…
fix(ready): integrate readiness gate with dev startup sync (overtake)
fix(ready): integrate readiness gate with dev startup sync (overtake)
fix(ready): integrate readiness gate with dev startup sync (overtake)
fix(codex): warn when a startup write leaves an app-server stale (lid…
Merge pull request lidge-jun#1072 from lidge-jun/codex/1046-startup-s…
fix(ready): integrate readiness gate with dev startup sync (overtake)