codex: retry without --profile when the first attempt fails fast - #184
Merged
Edwinhe03 merged 5 commits intoAug 10, 2026
Merged
Conversation
Edwinhe03
marked this pull request as ready for review
July 6, 2026 22:29
Edwinhe03
force-pushed
the
edwin-he/ucode-composable-flag-injection
branch
from
July 7, 2026 22:31
c577f42 to
bcff2ab
Compare
Edwinhe03
force-pushed
the
edwin-he/ucode-composable-flag-injection
branch
from
July 7, 2026 22:56
bcff2ab to
7d540fb
Compare
Collaborator
|
am i testing this right? it seems to error on start? i checked out your branch no app-server: with app-server |
Collaborator
|
Collaborator
|
follow up: |
Edwinhe03
force-pushed
the
edwin-he/ucode-composable-flag-injection
branch
from
July 8, 2026 23:32
7d540fb to
a9961a2
Compare
Edwinhe03
force-pushed
the
edwin-he/ucode-composable-flag-injection
branch
from
July 9, 2026 00:32
a9961a2 to
b37ab3d
Compare
lilly-luo
reviewed
Jul 10, 2026
lilly-luo
reviewed
Jul 10, 2026
Edwinhe03
force-pushed
the
edwin-he/ucode-composable-flag-injection
branch
3 times, most recently
from
July 13, 2026 23:35
63da551 to
52830b0
Compare
lilly-luo
reviewed
Jul 15, 2026
lilly-luo
reviewed
Jul 15, 2026
lilly-luo
reviewed
Jul 15, 2026
Edwinhe03
force-pushed
the
edwin-he/ucode-composable-flag-injection
branch
from
July 15, 2026 20:06
52830b0 to
881ca21
Compare
ucode's codex launch always ran `codex --profile ucode <args>`, but codex only accepts the global `--profile` on runtime subcommands. Server-family subcommands (app-server, mcp-server, exec-server, remote-control) reject it up front: Error: --profile only applies to runtime commands and `codex mcp`: ... so e.g. `ucode codex app-server` was functionally broken. Run codex with `--profile` first; if that attempt exits nonzero *and* does so fast (< 3s), relaunch without `--profile`. No stderr capture, no temp file, no subcommand allow-list to maintain. The signal is timing, not the error text: codex's `--profile` rejection is a CLI parse-time error (~0.15s, before it touches auth/gateway/network), whereas a session that actually starts can only fail after a network round-trip (seconds) — there is no overlap, and the exit code alone can't tell them apart (both are 1). The fast-failure gate is what keeps this strictly better than the status quo: without it, a genuinely-failing `codex exec` would be silently re-run without `--profile` — i.e. on the user's own OpenAI login, since ucode writes a *named-profile* file and no `--profile` means no ucode routing. stdio is inherited (no capture), so Ctrl-C reaches codex directly and quitting an interactive session propagates a KeyboardInterrupt past the retry check rather than tripping it. Co-authored-by: Isaac
Edwinhe03
force-pushed
the
edwin-he/ucode-composable-flag-injection
branch
from
July 20, 2026 19:47
881ca21 to
4d5de9c
Compare
AarushiShah-db
previously approved these changes
Aug 3, 2026
AarushiShah-db
left a comment
Collaborator
There was a problem hiding this comment.
This should be fine, but can we print a warning to the user that we are falling back to their local settings defined in ~/.codex/config.toml
The fallback relaunches codex without --profile, which drops ucode's Databricks routing (ucode writes a *named-profile* file, so no --profile means codex resolves ~/.codex/config.toml and its own provider). Reviewers accepted that tradeoff for server-family subcommands on the condition the user is told, so say it explicitly. The warning goes to stderr, not stdout: the subcommand that reaches this path is `codex app-server`, whose stdout is a JSON-RPC stream its caller parses, and a warning there would corrupt it. Adds print_warning_err to ui.py rather than reusing print_err, which carries the wrong severity. Emitted before exec_or_spawn, since execvp replaces the process and anything buffered after would be lost. Co-authored-by: Isaac
The prior wording assumed the reader knew what --profile was. But users never type it — ucode injects it — so codex's own "Error: --profile only applies to runtime commands" line reads as a scary error about a flag they didn't write, with no hint that ucode caused it or that a retry follows. Reword to lead with disowning that error, name ucode as the source of the flag, say a retry is happening, and end on the consequence (local Codex settings, no Databricks gateway). Suppressing codex's line instead would mean capturing stderr on the first attempt, which would swallow real session errors when the attempt is not a --profile rejection. Co-authored-by: Isaac
The prior wording explained itself at five wrapped lines, which is more than a warning should spend. Keep the four things that matter — ucode owns the flag, codex's error above is the rejection, a retry is happening, and the run loses Databricks routing — and cut the rest. Co-authored-by: Isaac
Edwinhe03
enabled auto-merge (squash)
August 7, 2026 21:28
AarushiShah-db
approved these changes
Aug 10, 2026
Edwinhe03
disabled auto-merge
August 10, 2026 23:02
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
ucode codex's launch always rancodex --profile ucode <args>. But codex only accepts the global--profileon runtime subcommands. All four server-family subcommands —app-server,mcp-server,exec-server,remote-control(each verified to reject on 0.146.1) — fail immediately:So
ucode codex app-server(and the other server subcommands) was functionally broken. Note the flag is ucode's own — the user never typed it — which is what makes that error confusing in the field, and why the fallback warning (below) attributes it.Change
Dead simple, inline in
launch(): run codex with--profilefirst; if that attempt exits nonzero AND does so fast (< 3s), warn and relaunch without--profile. No stderr capture, no temp file, no subcommand allow-list to maintain.The signal is timing, not the error text. codex's
--profilerejection is a CLI parse-time error — it happens before codex touches auth, the gateway, or the network — so it exits in ~0.1–0.15s (measured on 0.137/0.141/0.144, re-confirmed on 0.146.1). A codex command that actually starts a session can only fail after a network round-trip, i.e. seconds. The exit code alone can't distinguish them (the rejection and an ordinary failure both exit1), so elapsed time is what we key on.The fast-failure gate is what makes this better than an unconditional retry. Without it, a genuinely-failing
codex exec "prompt"(transient gateway error, bad model) would be silently re-run without--profile. Since ucode writes a named-profile file (ucode.config.toml, not the defaultconfig.toml), no--profilemeans no ucode routing — codex falls back toprovider: openai, i.e. the user's own OpenAI login. The gate keeps a real session failure (seconds) from being retried at all.stdio is inherited (no capture), so Ctrl-C reaches codex directly; quitting an interactive session raises
KeyboardInterruptthat propagates past the retry check, so a normal quit is never mistaken for a rejection.Self-adapting across codex versions: a future runtime subcommand keeps
--profile(accepted, no fast failure); a future server subcommand falls back (fast reject). No ucode change needed either way.Known limitation, and why we warn
The timing gate narrows the misroute window but does not close it. A subcommand that does accept
--profilecan still fail fast for a non---profilereason — e.g.codex execfailing on a malformed local token before any network I/O — and the retry would then re-run it without ucode's routing, on the user's own OpenAI credentials.Per review discussion, the alternative (overwriting the user's
~/.codex/config.tomlso--profileis unnecessary) was rejected as surprising, and this tradeoff was accepted on the condition that the fallback is not silent. So the fallback now prints a warning naming the config that will be used instead.Users never type
--profile— ucode injects it — so the warning attributes the flag to ucode rather than letting codex's rejection read as the user's mistake.Actual output, captured from a real
ucode codex app-serverrun against codex 0.146.1 — both codex's rejection and ucode's warning on stderr, with stdout byte-empty:(Rich soft-wraps to terminal width; the path renders absolute via
LEGACY_CODEX_CONFIG_PATH.)Codex's own error line is left in place deliberately. Suppressing it would mean capturing stderr on the first attempt — the piping approach rejected earlier in review — and when the fast failure is not a
--profilerejection (a bad flag, say) that capture would swallow the real error the user needs to see.The warning goes to stderr, not stdout. The subcommand that reaches this path is
codex app-server, whose stdout is a JSON-RPC stream its caller (e.g. omnigent) parses — a warning there would corrupt the stream this PR exists to unblock. It is emitted beforeexec_or_spawn, sinceexecvpreplaces the process and anything buffered after would be lost.ui.pygainsprint_warning_errfor this (stderr sibling ofprint_warning;print_errcarries the wrong severity).Touches
codex.py,ui.py, andtest_agent_codex.py;launcher.pyis untouched.Verification
ruff+tyclean.launch()tests (fakesubprocess.run+ monkeypatchedtime.monotonic) cover: runs with--profile+ sets OAUTH_TOKEN; success → no retry; fast nonzero (app-server/mcp-server) → relaunch without--profile; slow nonzero → NO retry (exit propagated); fast zero-exit → no retry; and the fallback warning — asserted to land on stderr with stdout empty, and to be emitted before the handoff.--profilerejection exits in 0.11 / 0.15 / 0.13s across three runs (bounded by node cold-start), always exit 1; a realexecagainst a bad gateway fails in ~25s. ~180× separation.codex.launch()against the real codex binary (only the token fetch and the finalexec_or_spawnhandoff stubbed, so the process survives to be inspected):app-server:--profilerejected fast → warning emitted → fallback reached. stdout measured at 0 bytes (wc -c), so the JSON-RPC stream is intact; the warning and codex's error are both on stderr.exec: no warning, no retry, exit 1 propagated — the misroute the gate prevents.app-server --listen: relaunch → socket bound, server alive.Note:
tests/test_e2e_user_agent.py::TestClaudeUserAgent::test_user_agent_arrives_at_gatewayfails on this branch, but also fails identically on a clean tree — pre-existing and unrelated (Claude user-agent header against the live gateway).This pull request and its description were written by Isaac.