Skip to content

Launch Codex sessions with <Session.Launch> (#755) - #764

Draft
taras wants to merge 1 commit into
agent/issue-732-tmux-gridfrom
agent/issue-755-codex-native-launch
Draft

Launch Codex sessions with <Session.Launch> (#755)#764
taras wants to merge 1 commit into
agent/issue-732-tmux-gridfrom
agent/issue-755-codex-native-launch

Conversation

@taras

@taras taras commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Why

Codex is a known native-session provider, but <Session.Launch> cannot select
it because the adapter has not established that a conversation created through
ACP can be handed to codex resume without 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:

  • codex has a known native resume command but is not advertised for
    <Session.Launch>.
  • Trying to launch it refuses before allocating or resuming a conversation.

After:

  • Deno and compiled XMD advertise Codex 0.153.2 with ACP adapter 1.6.2 for
    native launch.
  • A first launch creates the conversation through ACP, announces and submits
    exactly one XMD-owned codex-materialization.v1 turn, releases ACP ownership,
    and resumes that exact native identity in Codex's terminal UI.
  • The materialized phase is journaled, so incomplete replay reuses the retained
    identity without buying another materialization turn; completed replay starts
    no process.
  • Exiting the native UI before a user-authored turn still leaves the
    materialized conversation resumable.
  • Missing identities, ownership conflicts, unsupported builds, permission or
    working-directory drift, and explicit missing-session refusals fail closed
    without a replacement conversation.

How it works

<Session.Launch> → create/reattach through ACP → fixed materialization turn
                 → release ACP ownership → codex resume <same identity>
                 → native exit → retained resumable session

The launch journal records prepared, materialized, and detached phases.
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.ts

Then review:

  1. packages/core/src/agent/launch.ts and the materialized launch phase
  2. packages/acp/src/native-launch.ts, provider identity ownership, and Codex
    advertisement
  3. packages/acp/tests/fixtures/codex-surfaces.ts and
    terminal-screen.ts for terminal/refusal decisions
  4. packages/acp/src/CodexNativeLaunch.test.md and
    CodexZeroNativeTurnExit.test.md for the opt-in real-provider boundary
  5. the updated architecture and native-launch specifications

Look carefully at:

  • no typing, consent, Enter, or turn charge may follow a committed
    No saved session found with ID surface, even if Codex repaints it in the
    same read or during a fixed delay;
  • ACP ownership ends before the native UI attaches;
  • replay never allocates or materializes twice;
  • unsupported compatibility points refuse instead of falling back to a picker,
    --last, or a new conversation.

What must stay true

  • The materialization payload has fixed bytes and exactly one owner — enforced
    by the launch journal and checked by the focused launch and replay suites.
  • Provider-returned identity is authoritative — mismatches and missing sessions
    return failures rather than inferring or substituting an identity.
  • Prepared instructions do not enter argv or environment — the ACP preparation
    and adapter protocol tests inspect both boundaries.
  • The terminal decides from presented screens, not retained output bytes or
    held frames — CS/NA terminal tests cover repaints, same-read frames, held
    frames, fixed delays, and mutations of each refusal gate.
  • Claude's client-allocated native launch path remains unchanged — existing
    Claude and acceptance suites run alongside the Codex cases.

How to verify it

  • deno task test --changed=origin/agent/issue-732-tmux-grid selects the branch
    impact 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.
  • Focused Codex surface and terminal tests pass under Deno, Node, and Bun. They
    fail under mutations that drop refusal-aware waits, current-screen checks,
    delay observation, committed-frame retention, or held-frame isolation.
  • deno task check and deno task lint pass after deno task fmt.
  • Unarmed CodexNativeLaunch.test.md and CodexZeroNativeTurnExit.test.md pass
    with modelTurns: 0.
  • The armed CodexNativeLaunch.test.md real-provider rerun is intentionally
    pending 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

  • Codex native-launch advertisement for the tested compatibility point
  • the fixed one-turn materialization and retained launch phase
  • provider-returned identity, ownership, replay, and refusal behavior
  • root and terminal-pane launch integration
  • opt-in native and zero-user-turn executable proofs
  • architecture and specification updates

Intentionally unchanged

New abstractions

  • The materialized launch phase exists because Codex must durably own one
    completed turn before its provider-returned identity is resumable.
  • Terminal presented-frame watches exist because a single PTY read can commit,
    erase, and replace a decisive refusal before driver code resumes.
  • Delay-scoped presentation observation exists because Codex keeps drawing
    during trust settling and startup grace, when a plain sleep would lose those
    committed frames.
  • Each new abstraction has concrete launch/proof consumers.
  • No speculative functionality is included.

Generated or mechanical changes

  • The vendored Codex ACP adapter archive and generated snapshot are refreshed
    from the recorded compatibility source; MANIFEST.json and PROVENANCE.md
    record their integrity and origin.

Risks and limitations

  • The compatibility advertisement is intentionally pinned to the tested Codex
    and adapter versions; other builds are not silently assumed compatible.
  • The materialization turn is real account usage and is announced before it is
    submitted. Replay prevents XMD from spending it twice for one retained
    launch.
  • Recovery is to remove the Codex advertisement while retaining the
    fail-closed provider boundary.

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

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.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 7 redundant comments. Inline suggestions to remove them below.

codex: {
launcher: "codex",
// Codex creates the conversation through ACP and reports what it is called.
// XMD supplies nothing here and accepts only that assertion.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// by this ensure, bound to the build that names it.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

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.
Symbol Declared at Refs in diff Why flagged
LaunchPhase packages/core/src/agent/launch.ts:37 1 referenced ≤1× within the added diff (pre-existing usages not counted)

Oxlint structural signals:

  • no-unused-vars ×13: packages/acp/src/provider.ts, packages/acp/tests/fixtures/codex-native-launch-proof.ts, packages/core/src/agent/launch-owner.ts (+1)
  • no-redundant-type-constituents ×4: packages/runtime/launcher.ts, packages/acp/tests/fixtures/codex-native-launch-proof.ts
  • no-empty-function ×3: packages/acp/src/provider.ts, packages/runtime/launcher.ts
  • no-unnecessary-type-assertion ×2: packages/core/src/expand.ts

Slop

  • packages/acp/src/native-launch.ts:263// XMD supplies nothing here and accepts only that assertion.
  • packages/acp/src/native-launch.ts:274// build the native UI then resumes it with.
  • packages/acp/src/native-launch.ts:281// forbids the work the session was prepared for.
  • packages/acp/src/provider.ts:1202// serve one, the establishment itself reports being unable to.
  • packages/acp/src/provider.ts:1876// there is nothing to compare and nothing to continue.
  • packages/acp/src/provider.ts:1926// provider state — and existing history is never reclassified.
  • packages/acp/src/provider.ts:2246// by this ensure, bound to the build that names it.

Oxlint slop signals:

  • no-inferrable-types ×2: packages/core/src/expand.ts

Static Analysis

Oxlint: 72 diagnostics across 9 files (20 rules)
Density: 0.005 violations/added-line

consistent-function-scoping (14): packages/acp/src/provider.ts, packages/acp/tests/fixtures/codex-native-launch-proof.ts
no-unused-vars (13): packages/acp/src/provider.ts, packages/acp/tests/fixtures/codex-native-launch-proof.ts, packages/core/src/agent/launch-owner.ts (+1)
unbound-method (6): packages/core/src/agent/launch-authority.ts, packages/acp/tests/fixtures/codex-native-launch-proof.ts, packages/core/src/expand.ts (+1)
no-shadow (5): packages/acp/src/provider.ts, packages/core/src/expand.ts, packages/runtime/launcher.ts
no-unsafe-type-assertion (5): packages/acp/src/session-route.ts, packages/core/src/expand.ts
no-redundant-type-constituents (4): packages/runtime/launcher.ts, packages/acp/tests/fixtures/codex-native-launch-proof.ts
no-base-to-string (4): packages/core/src/expand.ts
no-useless-spread (3): packages/acp/src/provider.ts
no-empty-function (3): packages/acp/src/provider.ts, packages/runtime/launcher.ts
no-floating-promises (3): packages/acp/src/provider.ts, packages/acp/tests/fixtures/codex-native-launch-proof.ts
no-inferrable-types (2): packages/core/src/expand.ts
no-unnecessary-type-assertion (2): packages/core/src/expand.ts
no-new-array (1): packages/acp/tests/fixtures/terminal-screen.ts
no-array-sort (1): packages/acp/src/native-launch.ts
no-useless-escape (1): packages/acp/tests/fixtures/codex-native-launch-proof.ts
no-control-regex (1): packages/acp/tests/fixtures/codex-native-launch-proof.ts
no-useless-fallback-in-spread (1): packages/core/src/expand.ts
restrict-template-expressions (1): packages/core/src/expand.ts
no-unnecessary-template-expression (1): packages/acp/src/provider.ts
no-implied-eval (1): packages/core/src/expand.ts

Correctness

No extraneous code patterns detected.

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