codex: try --profile, fall back on codex's rejection error#184
codex: try --profile, fall back on codex's rejection error#184Edwinhe03 wants to merge 1 commit into
Conversation
c577f42 to
bcff2ab
Compare
bcff2ab to
7d540fb
Compare
|
am i testing this right? it seems to error on start? i checked out your branch no app-server: with app-server |
|
|
follow up: |
7d540fb to
a9961a2
Compare
a9961a2 to
b37ab3d
Compare
| _CODEX_PROFILE_SUBCOMMANDS = frozenset( | ||
| {"exec", "review", "resume", "archive", "delete", "unarchive", "fork", "mcp", "sandbox"} | ||
| ) |
There was a problem hiding this comment.
will we also need to maintain this list ? it also will depend on the cli version of codex that the customer is on
There was a problem hiding this comment.
yes, I can't see how we can get around this. but problem statement is ucode unconditionally adds --profile to all codex commands when codex doesn't support --profile on all subcommands. do you have any ideas?
There was a problem hiding this comment.
can you just do a try catch on the specific error?
63da551 to
52830b0
Compare
| # codex accepts the global --profile only on its runtime subcommands (plus the | ||
| # bare interactive TUI); server-family subcommands (app-server, mcp-server, | ||
| # exec-server, remote-control) and utilities reject it up front by printing | ||
| # this to stderr and exiting nonzero. The accepted set drifts across codex | ||
| # versions, so rather than maintain a version-dependent list we try with | ||
| # --profile and fall back exactly on this rejection. Stable prefix verified on | ||
| # codex 0.137 and 0.141; if a future codex rewords it, the launch fails loudly | ||
| # with codex's own error instead of silently dropping ucode's routing. |
There was a problem hiding this comment.
can you make this less verbose? or just rm this comment
| stderr write, so nothing has to read alongside the running process. The | ||
| trade is that codex's stderr appears when it exits, not live. | ||
| """ | ||
| with tempfile.TemporaryFile() as stderr_file: |
There was a problem hiding this comment.
why do we need to wrap in this temp file? it seems diff behavior from before - exec_or_spawn doesnt have the temp file
There was a problem hiding this comment.
exec_or_spawn doesn't capture any output. we could do piping, but then need to consider error output streaming and pipe buffer
| _exec_or_spawn_with_fallback( | ||
| [binary, "--profile", CODEX_PROFILE_NAME, *tool_args], | ||
| [binary, *tool_args], | ||
| _PROFILE_REJECTED_MARKER, | ||
| ) |
There was a problem hiding this comment.
instead of a new method, is it possible to do something inline + more simple like:
try:
exec_or_spawn
except Exception as e:
if e.msg == _PROFILE_REJECTED_MARKER:
exec_or_spawn([binary, *tool_args])
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) and utilities reject it up front: Error: --profile only applies to runtime commands and `codex mcp`: ... so e.g. `ucode codex app-server` was functionally broken. Rather than maintain a version-dependent allow-list of which subcommands accept `--profile` (the accepted set drifts across codex releases), catch codex's own rejection: launch() spawns codex with `--profile`, capturing stderr to a temp file, and reads it once codex exits. Only a nonzero exit whose stderr contains the rejection message relaunches without `--profile` (swallowing the expected noise); any other outcome replays the captured stderr and propagates the exit code unchanged, so an unrelated error is never silently retried without ucode's routing. A temp file, unlike a pipe, has no backpressure, so codex can never block on a stderr write. The rejection is instant and side-effect-free (verified on codex 0.137 and 0.141), so the fallback costs ~150ms once. Inlined directly in launch() (no separate helper); launcher.py is untouched. Co-authored-by: Isaac
52830b0 to
881ca21
Compare
What
ucode codex's launch always rancodex --profile ucode <args>. But codex only accepts the global--profileon runtime subcommands. Server-family subcommands (app-server,mcp-server,exec-server,remote-control) and utilities reject it immediately:So
ucode codex app-server(and the other server subcommands) was functionally broken.Change
No version-dependent lists to maintain — instead of an allow-list, catch codex's own rejection, inline in
launch():launch()spawns codex with--profilefirst, with stderr captured to a temp file, read once codex exits. A nonzero exit whose stderr contains codex's rejection message (--profile only applies to runtime commands) relaunches without--profile, swallowing the expected noise. Rejected subcommands are caller-configured anyway (omnigent runscodex app-serverwith its ownCODEX_HOME), so omitting--profilethere just stays out of the way.A temp file, unlike a pipe, has no backpressure: codex can never block on a stderr write, so nothing has to read alongside the running process (no pump thread). The trade is that codex's stderr appears at exit rather than live — and at exit is exactly when failures, the case where stderr matters, are read anyway. Ctrl-C is forwarded to codex just as
exec_or_spawndoes on its Windows path.This is self-adapting: a future codex version that adds a new runtime subcommand gets
--profilecorrectly on the first attempt, and a new server subcommand falls back — neither requires a ucode change. The rejection is instant and side-effect-free, so the fallback costs ~150ms once. If codex ever rewords the error, the launch fails loudly with codex's own message rather than silently misrouting.The whole thing is inlined in
launch()(no separate helper) and touches onlycodex.py;launcher.pyis untouched.Verification
ruff+tyclean.launch()tests drive a fakePopenand cover: spawns with--profile+ sets OAUTH_TOKEN; success → stderr replayed, no fallback;--profilerejection → relaunch without it, rejection swallowed; unrelated failure → propagated, no fallback; marker on a zero exit → no fallback.codex.launch()against real codex:app-server --listen: first attempt rejected → relaunch → socket bound, server alive, and the rejection line is absent from output (swallowed).exec --help: accepted first try, exit 0, no fallback.ucode.config.toml+exec: original error replayed to the caller, exit 1, no fallback (no silent bypass of the Databricks gateway).Manual verification (real
ucode codexCLI)Configured
ucodeisolated against a live staging workspace (ucode configure --profiles <p> --agents codex --use-pat), then drove the real CLI:ucode codex app-server --listen unix://…(the previously-broken case): first attempt rejected → relaunch → app-server socket bound and process alive, with the rejection line absent from output. Only ucode's normal launch banner is printed.ucode codex mcp list(runtime subcommand): accepted with--profileon the first attempt, exit 0, no fallback.ucode codex exec --skip-git-repo-check "Reply with exactly: CODEX_OK": full model round-trip through the Databricks AI Gateway on the pinned model (system.ai.gpt-5-2) — repliedCODEX_OKin 18s, proving--profilerouting is intact end-to-end.This pull request and its description were written by Isaac.