Skip to content

code-mode: add process-owned session runtime - #29805

Closed
cconger wants to merge 6 commits into
mainfrom
cconger/process-owned-session-runtime
Closed

code-mode: add process-owned session runtime#29805
cconger wants to merge 6 commits into
mainfrom
cconger/process-owned-session-runtime

Conversation

@cconger

@cconger cconger commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Why

Code mode needs a process-owned implementation that preserves the existing CodeModeSession contract while moving JavaScript execution and stored session values out of the client process. The host and client are introduced together here so the transport is reviewable as one end-to-end path rather than as disconnected process halves.

What changed

  • adds the codex-code-mode-host sidecar binary
  • lazily spawns and negotiates one process host for a provider
  • multiplexes logical sessions and request/response correlation over framed stdin/stdout
  • implements session open, execute, wait, terminate, and shutdown
  • hosts each logical session with InProcessCodeModeSession
  • forwards nested tool calls and notifications through a remote delegate
  • transports the delayed initial runtime response separately from execution admission
  • shuts hosted sessions down when the connection ends
  • adds end-to-end coverage for stored values, delegates, and cell control

This does not activate the process-owned provider in the production tool path. Packaging and binary discovery remain follow-up work.

Testing

  • just test -p codex-code-mode
  • just test -p codex-code-mode-host

Stack

Part 2 of 6. Depends on #29804; followed by #29806.

self.state.pending.lock().await.remove(&id);
return Err(err);
}
response_rx

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This isn’t cancellation-safe: once the frame is sent, dropping the caller only drops response_rx

let Some(message) = message else {
break;
};
if let Err(err) = writer.write(&message).await {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This makes an oversized client→host message connection-fatal. A nested-tool result can exceed the frame cap, so one large MCP/image/exec result tears down every session sharing this host
Is this on purpose? (not sure what's the latest decision here)

for (_, sender) in self.initial_responses.lock().await.drain() {
let _ = sender.send(Err(reason.clone()));
}
self.delegates.lock().await.clear();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Clearing the delegates skips cell_closed for every live remote cell

let response = self
.request(HostRequest::OpenSession {
session_id: session_id.clone(),
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There’s an open-session leak here if create_session() is cancelled: neither cleanup arm runs, so the host can retain the session and this map retains the delegate after the only local handle is gone

.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match std::mem::replace(&mut *state, SessionState::Shutdown) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shutdown is published before the host acknowledges cleanup... this is race prone

.write(&ClientToHost::ClientHello(hello))
.await
.map_err(|err| format!("failed to write code-mode host hello: {err}"))?;
match reader

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we put the startup hello exchange behind a finite deadline and start draining stderr before awaiting it?

});
let writer_task = tokio::spawn(async move {
while let Some(message) = outgoing_rx.recv().await {
writer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(found by Codex)
The reverse direction has the same blast radius: JS can build an arbitrarily large tool argument or notify() string, which becomes one DelegateRequest. If it exceeds MAX_FRAME_BYTES, this stops the writer and tears down the shared host

@cconger
cconger force-pushed the cconger/process-owned-session-runtime branch 2 times, most recently from 0f7bda8 to 537cda9 Compare June 24, 2026 21:45
@cconger
cconger force-pushed the cconger/process-owned-session-protocol branch from e0fac5a to 125a59b Compare June 24, 2026 22:26
@cconger
cconger force-pushed the cconger/process-owned-session-runtime branch from 537cda9 to 42691b9 Compare June 24, 2026 22:26
@cconger
cconger force-pushed the cconger/process-owned-session-protocol branch from 125a59b to c52c04b Compare June 25, 2026 01:08
@cconger
cconger force-pushed the cconger/process-owned-session-runtime branch from 42691b9 to 1886ad6 Compare June 25, 2026 01:08
cconger added a commit that referenced this pull request Jun 25, 2026
## Why

The process-owned code mode implementation needs an explicit, bounded
wire contract before either side depends on it. Keeping framing and
message semantics in `codex-code-mode-protocol` gives the client and
sidecar one shared source of truth and makes compatibility failures
detectable during connection setup.

## What changed

- adds a versioned client/host handshake with required and optional
capabilities
- defines operation requests and responses for session lifecycle and
cell control
- defines reverse delegate request, response, cancellation, and
cell-closure messages
- adds a four-byte little-endian length-prefixed JSON codec with a hard
frame cap
- rejects malformed frames, unknown fields, invalid identifiers, and
unsupported protocol states
- locks the wire representation down with explicit JSON round-trip tests

## Testing

- `just test -p codex-code-mode-protocol`

## Stack

Part 1 of 6. Followed by
[#29805](#29805).
Base automatically changed from cconger/process-owned-session-protocol to main June 25, 2026 03:03
@cconger cconger closed this Jun 26, 2026
jacks0n pushed a commit to jacks0n/codex that referenced this pull request Jul 6, 2026
## Why

The process-owned code mode implementation needs an explicit, bounded
wire contract before either side depends on it. Keeping framing and
message semantics in `codex-code-mode-protocol` gives the client and
sidecar one shared source of truth and makes compatibility failures
detectable during connection setup.

## What changed

- adds a versioned client/host handshake with required and optional
capabilities
- defines operation requests and responses for session lifecycle and
cell control
- defines reverse delegate request, response, cancellation, and
cell-closure messages
- adds a four-byte little-endian length-prefixed JSON codec with a hard
frame cap
- rejects malformed frames, unknown fields, invalid identifiers, and
unsupported protocol states
- locks the wire representation down with explicit JSON round-trip tests

## Testing

- `just test -p codex-code-mode-protocol`

## Stack

Part 1 of 6. Followed by
[openai#29805](openai#29805).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants