Skip to content

feat(codex): run anywhere, declare a concurrency limit, resolve model aliases - #10

Merged
pedromvgomes merged 4 commits into
mainfrom
feature/codex-max-concurrent-runs
Sep 7, 2026
Merged

feat(codex): run anywhere, declare a concurrency limit, resolve model aliases#10
pedromvgomes merged 4 commits into
mainfrom
feature/codex-max-concurrent-runs

Conversation

@pedromvgomes

Copy link
Copy Markdown
Owner

Three codex changes that all remove a reason a caller has to know which dialect
it is addressing. They ship together as v0.6.0.

codex exec always skips the git repository check

codex refuses to start outside a git repository — "Not inside a trusted
directory and --skip-git-repo-check was not specified"
— and nothing in this
library's surface let a caller lift that guard: no Request field, no option, no
argv passthrough. The comment naming it a caller's responsibility named a
responsibility with no mechanism to discharge it, and the driver's own
integration suite worked around it by git init-ing a temp directory.

The check is a guardrail for an interactive human who may have opened codex in
the wrong directory. codex exec is the scripted entry point: there is nobody
to warn, and where a scripted child runs is the caller's deliberate choice.
claudecode has no equivalent check, so the same Request succeeded on one
provider and died at spawn on the other.

The flag is now on every invocation, ahead of the positional prompt, the way
claudecode opens every argv with --setting-sources "". The integration
suite's workspace helper is a plain t.TempDir() — that it reaches the
prompt at all is the end-to-end proof.

A provider declares whether its runs can be concurrent

ConcurrencyLimiter is a new optional interface, discovered by type assertion
like every other capability. codex implements it and answers 1; claudecode
does not implement it, and absent means unconstrained.

codex authenticates from auth.json under CODEX_HOME — a file it rewrites in
place, whose refresh tokens are effectively single-use. Claude Code
authenticates from a static bearer token in an environment variable that nothing
rewrites. So "run four agents in parallel" was correct on one provider and
corrupted a credential on the other, and the only route left to a caller was
if id == "codex".

Three decisions, recorded in
ADR 0005:

  • On the provider, not on Driver. A driver could account for the
    credential mode it was built with, but the answer would be wrong where it
    matters most: a CODEX_HOME profile holding a session outranks the token
    Isolated injects, so "isolated, therefore unbounded" would be describing a
    profile the driver cannot see. Driver.MaxConcurrentRuns forwards the
    provider's answer and adds nothing, the way Driver.ResolveModel does.
  • WithConfigDir does not lift the limit. Copies of one session are not
    independent sessions — the same single-use refresh token is in every copy, so
    the first refresh invalidates the rest and can invalidate the source profile.
    Considered and rejected in the ADR, because it is the obvious wrong fix.
  • A count, not a boolean. A MustSerialise() bool states exactly what
    implementing the interface already states, leaving a caller sizing a pool with
    nothing to size it from. Installer.SigningIdentity is the precedent: a
    method carries a value.

codex resolves family aliases

Request.Model meant a family on claudecode and an exact vendor string on
codex, so a caller writing configuration had to know which dialect it was
addressing to know what it could write. codex/model.go mirrors
claudecode/model.go: astra, sol, terra, luna and mini resolve to
concrete builds, and anything unrecognised passes through, because the CLI is
the authority on what it accepts.

The vocabulary is OpenAI's own rather than one shared across providers. A
shared one would let a manifest swap providers without rewriting models, and it
would buy that by having this library assert that some OpenAI model is "the
sonnet one" — an editorial claim it has no standing to make, failing in the
direction resolution exists to prevent.

Downstream

agentic-toolkit's agtk code-review runs reviewers in a neutral working
directory that is not the reviewed code (its ADR 0007). Once it bumps to
v0.6.0 it can delete the git init of that directory, and replace its
if name == "codex" serialisation with Driver.MaxConcurrentRuns().

codex refuses to start outside a git repository — "Not inside a trusted
directory and --skip-git-repo-check was not specified" — and nothing in
this library's surface let a caller lift that guard: no Request field, no
option, no argv passthrough. The dialect asymmetry landed on the caller
instead, where the same Request succeeds on claudecode and dies at spawn
here.

The check is a guardrail for an interactive human who may have opened
codex in the wrong directory. `codex exec` is the scripted entry point:
there is nobody to warn, and where a scripted child runs is the caller's
deliberate choice. The flag now goes on every invocation, ahead of the
positional prompt, the way claudecode opens every argv with
--setting-sources "".
A caller that wants to run several agents at once had no way to learn
that it must not. codex authenticates from auth.json under CODEX_HOME, a
file it rewrites in place whose refresh tokens are effectively
single-use; Claude Code authenticates from a static bearer token in an
environment variable that nothing rewrites. So the same fan-out is
correct on one provider and corrupts a credential on the other, and the
only route left to a caller was a switch on the provider ID.

ConcurrencyLimiter is that capability, discovered by type assertion like
every other. codex answers 1; claudecode does not implement it, and
absent means unconstrained. It carries a count rather than a boolean
because a boolean states only what the assertion already states, and it
lives on the provider rather than the Driver because a Codex profile
holding a session outranks the token Isolated injects — a driver
answering "isolated, therefore unbounded" would be describing a profile
it cannot see. docs/adr/0005 records both, and why a config dir per run
is not a way around the limit.
Request.Model meant two different things depending on the provider: a
family on claudecode, an exact vendor string on codex, so a caller
writing configuration had to know which dialect it was addressing to know
what it could write.

codex now implements ModelResolver over a table of OpenAI's own family
names, and anything unrecognised — a concrete ID and an unknown family
alike — is passed through, because the CLI is the authority on what it
accepts and rejecting here would make a model shipped after this file
unreachable through it.

The vocabulary is deliberately not shared with claudecode. A "sonnet"
that also meant something here would have this library assert that one
vendor's model is the counterpart of another's, and a caller swapping
providers in a manifest would silently get a model nobody chose.
The orchestrator gains an unconditional merge_group trigger: a queued
pull request reports its required check from that event and no other, so
without it a queue waits forever on a check nothing can report.
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

bulwark

  • scan — no findings

  • coverage — go: 83.8% (baseline 83.6%); go patch: 100.0% (19/19 new lines; baseline 83.6%)

📦 Full bulwark output — complete scan and coverage logs.

@pedromvgomes
pedromvgomes merged commit e3a64f8 into main Sep 7, 2026
9 checks passed
@pedromvgomes
pedromvgomes deleted the feature/codex-max-concurrent-runs branch September 7, 2026 19:24
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.

1 participant