Launch Codex sessions with <Session.Launch> (#755) - #764
Conversation
Codex CLI 0.153.2 writes a thread's rollout only once a turn has completed in it, so a launch that handed over the native UI without one would hand over a name `codex resume` disowns. XMD now spends exactly one turn of its own — `codex-materialization.v1`, fixed bytes, announced before it is spent — and then gets out of the way, which is what lets `codex` join `claude` as an advertised native-launch adapter. The turn is journaled as its own `materialized` phase so a second invocation can see the conversation is already openable and buy nothing. Adds two opt-in real-provider documents and the fixture behind them. The native proof decides whether Codex answered by reconstructing the terminal rather than searching the bytes it emitted. Codex addresses the cursor: it wraps, erases and repaints, so retained output holds text the screen no longer shows and an answer can be on screen in a shape no substring of the stream contains. `terminal-screen.ts` applies the pty's bytes to a screen of the pty's own size and reads that; a stall is now classified from the same screen. `composer-idle` stays what it always was — the terminal's state, never evidence that no answer appeared. What that screen is then asked lives in `codex-surfaces.ts`, because both questions the proof puts to it were wrong in ways only a paid run showed. Exhaustion is now a closed list of complete sentences rather than the substring `usage limit`, which Codex also says when it means the opposite: `You have 2 usage limit resets available` is an account with headroom to spare saying so, and reading it as a refusal blames an environment that never refused. Wording the list does not recognize is not exhaustion, so an unrecognized screen costs an investigation instead of a false claim about the product's environment — only a recognized refusal may report ENVIRONMENT_BLOCKED. Readiness is likewise a question about the screen. Codex draws its composer before it has a model, so a turn typed against arrived bytes can be queued rather than taken; the driver now waits for a composer with no trust dialog over it and no `model: loading` behind it, rechecks after the startup grace, and requires the typed turn to be present in the composer before Enter is reached. Every way that fails returns having typed at most, so no turn is spent and none is accounted for. Forty-four hand-written TUI streams decide what the observer and the classifier do with each shape offline, with no Codex process and no model turn.
| codex: { | ||
| launcher: "codex", | ||
| // Codex creates the conversation through ACP and reports what it is called. | ||
| // XMD supplies nothing here and accepts only that assertion. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // XMD supplies nothing here and accepts only that assertion. |
| // | ||
| // The first thing the Codex ACP adapter consults when deciding which | ||
| // Codex to run, so the build that creates the session through ACP is the | ||
| // build the native UI then resumes it with. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // build the native UI then resumes it with. |
| // resume <id>` reads rollouts — so a thread ACP created and nothing has | ||
| // spoken in is refused by name. One turn closes exactly that gap and | ||
| // nothing else, which is why the prompt asks for an acknowledgement and | ||
| // forbids the work the session was prepared for. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // forbids the work the session was prepared for. |
| // child of a build nothing has observed, which is the one thing a bound | ||
| // session may not talk to. Nothing on that path needs the answer: a | ||
| // client-native session is created by a native process, and where ACP does | ||
| // serve one, the establishment itself reports being unable to. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // serve one, the establishment itself reports being unable to. |
| if (route.schema !== "session-route.v3") { | ||
| // Constructed before any build was recorded. A build observed now says | ||
| // which build is installed today, not which one issued this identity, so | ||
| // there is nothing to compare and nothing to continue. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // there is nothing to compare and nothing to continue. |
| } | ||
| const agentCommand = agentCommandOf(prepared); | ||
| // An existing managed entry, or a durable record ACPX already kept, is | ||
| // provider state — and existing history is never reclassified. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // provider state — and existing history is never reclassified. |
| // established placement has one of its own. | ||
| // established placement has one of its own. A build alone does not | ||
| // defer anything — a session the provider names is still constructed | ||
| // by this ensure, bound to the build that names it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // by this ensure, bound to the build that names it. |
PR #764: Launch Codex sessions with <Session.Launch> (#755)38 files, +13348 / -3131 Scope🔴 PR has 16479 lines changed. Split into focused PRs. 🟡 16479 lines changed. PRs under 400 receive more thorough review. 🟡 38 files changed. Are all changes related? 🟡 Changes span 7 directories. Structural🟡 Type declarations with no consumers: LaunchPhase.
Oxlint structural signals:
Slop
Oxlint slop signals:
Static AnalysisOxlint: 72 diagnostics across 9 files (20 rules) consistent-function-scoping (14): packages/acp/src/provider.ts, packages/acp/tests/fixtures/codex-native-launch-proof.ts CorrectnessNo extraneous code patterns detected. |
Why
Codex is a known native-session provider, but
<Session.Launch>cannot selectit because the adapter has not established that a conversation created through
ACP can be handed to
codex resumewithout changing its identity, ownership,working directory, or permissions.
Testing the installed Codex 0.153.2 behavior established one additional
constraint: Codex does not persist an ACP-created thread until a turn completes
in it. XMD therefore needs one declared, fixed materialization turn before it
can release ACP ownership and hand the same durable conversation to the native
UI.
Closes #755.
What changes
Before:
codexhas a known native resume command but is not advertised for<Session.Launch>.After:
native launch.
exactly one XMD-owned
codex-materialization.v1turn, releases ACP ownership,and resumes that exact native identity in Codex's terminal UI.
identity without buying another materialization turn; completed replay starts
no process.
materialized conversation resumable.
working-directory drift, and explicit missing-session refusals fail closed
without a replacement conversation.
How it works
The launch journal records
prepared,materialized, anddetachedphases.The native proof reconstructs Codex's terminal screen instead of searching its
byte stream, because Codex redraws and erases frames in place. Screen waits and
fixed-delay observation retain every committed matching frame, including a
missing-session refusal that is repainted before the driver resumes.
Review guide
Start with:
packages/core/tests/agent-session-launch.test.tsThen review:
packages/core/src/agent/launch.tsand the materialized launch phasepackages/acp/src/native-launch.ts, provider identity ownership, and Codexadvertisement
packages/acp/tests/fixtures/codex-surfaces.tsandterminal-screen.tsfor terminal/refusal decisionspackages/acp/src/CodexNativeLaunch.test.mdandCodexZeroNativeTurnExit.test.mdfor the opt-in real-provider boundaryLook carefully at:
No saved session found with IDsurface, even if Codex repaints it in thesame read or during a fixed delay;
--last, or a new conversation.What must stay true
by the launch journal and checked by the focused launch and replay suites.
return failures rather than inferring or substituting an identity.
and adapter protocol tests inspect both boundaries.
held frames — CS/NA terminal tests cover repaints, same-read frames, held
frames, fixed delays, and mutations of each refusal gate.
Claude and acceptance suites run alongside the Codex cases.
How to verify it
deno task test --changed=origin/agent/issue-732-tmux-gridselects the branchimpact against this stacked PR's base.
deno test --allow-all --frozen packages/acp/exercises the full ACP package;the feedback revision passed 36 tests / 322 steps.
fail under mutations that drop refusal-aware waits, current-screen checks,
delay observation, committed-frame retention, or held-frame isolation.
deno task checkanddeno task lintpass afterdeno task fmt.CodexNativeLaunch.test.mdandCodexZeroNativeTurnExit.test.mdpasswith
modelTurns: 0.CodexNativeLaunch.test.mdreal-provider rerun is intentionallypending explicit authorization for its three Codex model turns. Two earlier
authorized runs exposed and then reproduced a harness classifier defect; they
did not establish a product verdict.
Scope
Included
Intentionally unchanged
boundary.
<Session.Launch>result capture remains owned by Return launch details from<Session.Launch>#753.New abstractions
completed turn before its provider-returned identity is resumable.
erase, and replace a decisive refusal before driver code resumes.
during trust settling and startup grace, when a plain sleep would lose those
committed frames.
Generated or mechanical changes
from the recorded compatibility source;
MANIFEST.jsonandPROVENANCE.mdrecord their integrity and origin.
Risks and limitations
and adapter versions; other builds are not silently assumed compatible.
submitted. Replay prevents XMD from spending it twice for one retained
launch.
fail-closed provider boundary.
Scope confirmation