Skip to content

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

Merged
Edwinhe03 merged 5 commits into
databricks:mainfrom
Edwinhe03:edwin-he/ucode-composable-flag-injection
Aug 10, 2026
Merged

codex: retry without --profile when the first attempt fails fast#184
Edwinhe03 merged 5 commits 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. All four server-family subcommands — app-server, mcp-server, exec-server, remote-control (each verified to reject on 0.146.1) — fail immediately:

Error: --profile only applies to runtime commands and `codex mcp`: `codex`, `codex exec`, `codex review`, `codex resume`, `codex archive`, `codex delete`, `codex unarchive`, `codex fork`, `codex mcp`, `codex sandbox`, and `codex debug prompt-input`.

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 --profile first; 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 --profile rejection 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 exit 1), 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 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 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 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.

Known limitation, and why we warn

The timing gate narrows the misroute window but does not close it. A subcommand that does accept --profile can still fail fast for a non---profile reason — e.g. codex exec failing 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.toml so --profile is 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-server run against codex 0.146.1 — both codex's rejection and ucode's warning on stderr, with stdout byte-empty:

$ ucode codex app-server
Error: --profile only applies to runtime commands and `codex mcp`: `codex`, `codex exec`, `codex review`, `codex resume`, `codex archive`, `codex delete`, `codex unarchive`, `codex fork`, `codex mcp`, `codex sandbox`, and `codex debug prompt-input`.
! ucode's `--profile` isn't accepted here (error above). Retrying without it:
this run uses /home/edwin.he/.codex/config.toml, NOT the Databricks gateway.

(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 --profile rejection (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 before exec_or_spawn, since execvp replaces the process and anything buffered after would be lost. ui.py gains print_warning_err for this (stderr sibling of print_warning; print_err carries the wrong severity).

Touches codex.py, ui.py, and test_agent_codex.py; launcher.py is untouched.

Verification

  • Full unit suite green (850 passed, 6 skipped, excluding live-gateway e2e); 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; and the fallback warning — asserted to land on stderr with stdout empty, and to be emitted before the handoff.
  • Timing measured empirically on codex 0.137 / 0.141 / 0.144, and re-confirmed on 0.146.1: the --profile rejection exits in 0.11 / 0.15 / 0.13s across three runs (bounded by node cold-start), always exit 1; a real exec against a bad gateway fails in ~25s. ~180× separation.
  • E2E driving the real codex.launch() against the real codex binary (only the token fetch and the final exec_or_spawn handoff stubbed, so the process survives to be inspected):
    • app-server: --profile rejected 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.
    • Slow (>3s) nonzero 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_gateway fails 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.

@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
@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 thread src/ucode/agents/codex.py Outdated
Comment thread src/ucode/agents/codex.py Outdated
@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
AarushiShah-db
AarushiShah-db previously approved these changes Aug 3, 2026

@AarushiShah-db AarushiShah-db left a comment

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.

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
Edwinhe03 enabled auto-merge (squash) August 7, 2026 21:28
@Edwinhe03
Edwinhe03 disabled auto-merge August 10, 2026 23:02
@Edwinhe03
Edwinhe03 merged commit b915144 into databricks:main Aug 10, 2026
2 checks passed
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.

3 participants