Skip to content

codex: retry without --profile when the first attempt fails fast#184

Open
Edwinhe03 wants to merge 1 commit into
databricks:mainfrom
Edwinhe03:edwin-he/ucode-composable-flag-injection
Open

codex: retry without --profile when the first attempt fails fast#184
Edwinhe03 wants to merge 1 commit into
databricks:mainfrom
Edwinhe03:edwin-he/ucode-composable-flag-injection

Conversation

@Edwinhe03

@Edwinhe03 Edwinhe03 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

What

ucode codex's 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 immediately:

Error: --profile only applies to runtime commands and `codex mcp`: `codex`, `codex exec`, ...

So ucode codex app-server (and the other server subcommands) was functionally broken.

Change

Dead simple, inline in launch(): 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 — it happens before codex touches auth, the gateway, or the network — so it exits in ~0.15s (measured, stable across 0.137/0.141/0.144). A codex command that actually starts a session can only fail after a network round-trip, i.e. seconds. There is no overlap, and the exit code alone can't distinguish them (the rejection and an ordinary failure both exit 1), so elapsed time is what we key on.

Why the fast-failure gate matters — this is what makes it strictly better than the status quo rather than a lateral move:

  • Without the gate, 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 default config.toml), no --profile means no ucode routing — codex falls back to provider: openai, i.e. the user's own OpenAI login. The gate ensures only the fast parse-time rejection is retried; a real session failure (seconds) is propagated untouched.
  • stdio is inherited (no capture), so Ctrl-C reaches codex directly; quitting an interactive session raises KeyboardInterrupt that 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.

Touches only codex.py + test_agent_codex.py; launcher.py is untouched.

Verification

  • Full unit suite green (852 passed, 8 skipped); ruff + ty clean. launch() tests (fake subprocess.run + monkeypatched time.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.
  • Timing measured empirically on codex 0.137 / 0.141 / 0.144: --profile rejection exits in ~0.13s (bounded by node cold-start); a real exec against a bad gateway fails in ~25s. ~180× separation.
  • E2E driving the real codex.launch():
    • app-server --listen: --profile rejected fast → relaunch → socket bound, server alive.
    • exec against a bogus gateway: fails slowly (25s), exit 1, and does NOT retry onto provider: openai — the misroute the gate prevents.

This pull request and its description were written by Isaac.

@Edwinhe03
Edwinhe03 marked this pull request as ready for review July 6, 2026 22:29
@rohita5l
rohita5l requested a review from lilly-luo July 7, 2026 13:19
@Edwinhe03 Edwinhe03 changed the title ucode: compose injected flags with the caller's command (claude --settings, codex --profile) ucode: compose claude --settings; passthrough non-interactive subcommands (codex app-server, --version/--help) Jul 7, 2026
@Edwinhe03
Edwinhe03 force-pushed the edwin-he/ucode-composable-flag-injection branch from c577f42 to bcff2ab Compare July 7, 2026 22:31
@Edwinhe03 Edwinhe03 changed the title ucode: compose claude --settings; passthrough non-interactive subcommands (codex app-server, --version/--help) ucode: exec the real binary for non-interactive subcommands (codex app-server, --version/--help) Jul 7, 2026
@Edwinhe03
Edwinhe03 force-pushed the edwin-he/ucode-composable-flag-injection branch from bcff2ab to 7d540fb Compare July 7, 2026 22:56
@lilly-luo

Copy link
Copy Markdown
Collaborator

am i testing this right? it seems to error on start?

i checked out your branch

no app-server:

$ uv run ucode codex
✔ Databricks auth already available for https://dbc-a5d4177a-49dc.cloud.databricks.com
✔ Unity AI Gateway detected      
                                
╭──────────────────╮
│ ucode with Codex │
╰──────────────────╯
  Model: system.ai.gpt-5-5
✔ Starting Codex
╭────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.143.0-alpha.14)                    │
│                                                        │
│ model:     system.ai.gpt-5-5 medium   /model to change │
│ directory: ~/ucode                                     │
╰────────────────────────────────────────────────────────╯

  Tip: New Build faster with Codex.

⚠ MCP startup interrupted. The following servers were not initialized: confluence, databricks-v2, devportal, github, glean,
  google, jira, logs-summariser, pagerduty, slack

with app-server

# lilly.luo at ip-10-93-41-23 in ~/ucode (git:edwin-he/ucode-composable-flag-injection) [14:50:55]
$ uv run ucode codex app-server
2026-07-08T14:50:56.919524Z ERROR codex_app_server: Project-local config, hooks, and exec policies are disabled in the following folders until the project is trusted, but skills still load.
    1. /home/lilly.luo/ucode/.codex
       To load project-local config, hooks, and exec policies, add /home/lilly.luo/ucode as a trusted project in /home/lilly.luo/.codex/config.toml.

2026-07-08T14:50:57.317311Z ERROR codex_models_manager::manager: failed to refresh available models: {"error_code":"BAD_REQUEST","message":"Request path '/codex/v1/models?client_version=0.143.0' doesn't match any known API type and is classified as an unmanaged api request. Set the Databricks-Model-Provider-Service header to the name of the model provider service to forward this request to."}

@lilly-luo

lilly-luo commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator
  1. could you help me understand why omnigent is even using ucode if it just wants the codex pass thru? why doesn't omnigent just call codex instead of ucode codex app-server
  2. instead of making this a separate command, would we make this a flag? something like the below. it could be nice for people to use ucode directlyucode codex --skip-init

@lilly-luo

Copy link
Copy Markdown
Collaborator

follow up:
Edwin: temp bump omnigent timeout
Lilly: introduce new config setting that skips all initialization, doesnt pull fresh models, doesn't test the tools

@Edwinhe03 Edwinhe03 closed this Jul 8, 2026
@Edwinhe03 Edwinhe03 changed the title ucode: exec the real binary for non-interactive subcommands (codex app-server, --version/--help) codex: allow-list --profile by subcommand; deliver config via -c where --profile is rejected Jul 8, 2026
@Edwinhe03 Edwinhe03 reopened this Jul 8, 2026
@Edwinhe03
Edwinhe03 force-pushed the edwin-he/ucode-composable-flag-injection branch from 7d540fb to a9961a2 Compare July 8, 2026 23:32
@Edwinhe03 Edwinhe03 changed the title codex: allow-list --profile by subcommand; deliver config via -c where --profile is rejected codex: allow-list --profile by subcommand; omit it where codex rejects it Jul 9, 2026
@Edwinhe03
Edwinhe03 force-pushed the edwin-he/ucode-composable-flag-injection branch from a9961a2 to b37ab3d Compare July 9, 2026 00:32
Comment thread src/ucode/agents/codex.py Outdated
Comment thread src/ucode/agents/codex.py Outdated
Comment on lines +448 to +450
_CODEX_PROFILE_SUBCOMMANDS = frozenset(
{"exec", "review", "resume", "archive", "delete", "unarchive", "fork", "mcp", "sandbox"}
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will we also need to maintain this list ? it also will depend on the cli version of codex that the customer is on

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you just do a try catch on the specific error?

@Edwinhe03 Edwinhe03 changed the title codex: allow-list --profile by subcommand; omit it where codex rejects it codex: try --profile, fall back on codex's rejection error Jul 13, 2026
@Edwinhe03
Edwinhe03 force-pushed the edwin-he/ucode-composable-flag-injection branch 3 times, most recently from 63da551 to 52830b0 Compare July 13, 2026 23:35
@Edwinhe03
Edwinhe03 requested a review from lilly-luo July 13, 2026 23:37
Comment thread src/ucode/agents/codex.py Outdated
Comment on lines +398 to +405
# 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make this less verbose? or just rm this comment

Comment thread src/ucode/agents/codex.py Outdated
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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to wrap in this temp file? it seems diff behavior from before - exec_or_spawn doesnt have the temp file

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exec_or_spawn doesn't capture any output. we could do piping, but then need to consider error output streaming and pipe buffer

Comment thread src/ucode/agents/codex.py Outdated
Comment on lines +462 to +466
_exec_or_spawn_with_fallback(
[binary, "--profile", CODEX_PROFILE_NAME, *tool_args],
[binary, *tool_args],
_PROFILE_REJECTED_MARKER,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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])

@Edwinhe03
Edwinhe03 force-pushed the edwin-he/ucode-composable-flag-injection branch from 52830b0 to 881ca21 Compare July 15, 2026 20:06
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 Edwinhe03 changed the title codex: try --profile, fall back on codex's rejection error codex: retry without --profile when the first attempt fails fast Jul 20, 2026
@Edwinhe03
Edwinhe03 force-pushed the edwin-he/ucode-composable-flag-injection branch from 881ca21 to 4d5de9c Compare July 20, 2026 19:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants