Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ Versioning and Keep a Changelog conventions.

## [Unreleased]

### Fixed

- Codex resumes and active-writer forks omit historical turns from their replies,
so long conversations can continue without exceeding the protocol frame limit.
Saved provider context is preserved.

### Changed

- Preserve Windows system and profile environment variables when launching providers,
Expand All @@ -24,6 +30,9 @@ Versioning and Keep a Changelog conventions.

### Added

- Add opt-in, bounded Codex app-server process reuse for in-process retained
runtimes, with strict runtime/configuration isolation, cold fallback at pool
capacity, idle expiry, and process-tree cleanup on cancellation or failure.
- Contain observer panic-payload cleanup failures and disable failed observers
across runtime clones. Report event-delivery wait separately from observed
first-text latency, preserving bounded backpressure.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ and questions are left unanswered. Production applications should implement
`InteractionHandler` and connect it to their durable approval workflow.

For a complete walkthrough, read the [quickstart](docs/tutorials/quickstart.md).
For long Codex threads, read [Resume large Codex conversations](docs/how-to/resume-large-codex-conversations.md).

For sandbox setup, read [Manage Nono sandboxes](docs/how-to/nono.md) or
[Implement a sandbox backend](docs/how-to/custom-sandbox.md). For durable
profile updates and retry, read
Expand Down
41 changes: 18 additions & 23 deletions docs/adr/0004-prepared-provider-processes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR 0004: Prepared provider processes

- Status: Proposed
- Status: Accepted (Codex retained turns implemented; explicit prewarming deferred)
- Date: 2026-09-23

## Problem
Expand All @@ -16,42 +16,37 @@ first assistant text. A first output frame is not evidence of provider readiness
or model request submission. Provider-specific readiness requires an explicit
handshake acknowledgement, not a sleep or an empty model turn.

## Proposed ownership
## Ownership

The SDK owns a bounded process supervisor and provider protocol state. Fleet owns
when a user has selected enough configuration to prepare, durable conversation
records, authorization, feature rollout, and presentation. Listing projects or
conversations must never spawn provider processes.

Existing `AgentRuntime::run` and retained-client constructors preserve lazy,
one-process-per-turn behavior. A separate opt-in client configuration enables
retained processes. Unsupported adapters and remote protocol versions report a
typed capability error rather than silently claiming preparation succeeded.
Existing `AgentRuntime::run` and default retained-client constructors preserve
lazy, one-process-per-turn behavior. `codex_process_retention` opts the in-process
retained client into bounded app-server reuse. Custom adapters remain disabled
unless they implement the lifecycle contract.

## Proposed lifecycle
## Lifecycle

1. Acquire a logical runtime with project, provider, sandbox and launch settings.
2. Explicitly prepare it, without a prompt or invocation identifier. This starts
the process and completes the provider handshake; it does not call a model,
create a synthetic transcript message, or grant tool execution.
3. Send a turn. Lazy sending performs preparation automatically. Sending during
preparation joins the same bounded operation and submits exactly once.
4. On a successful turn, keep the provider connection and continue draining its
2. Send the first real turn. This lazily starts and initializes the app server,
then submits the prompt exactly once.
3. On a successful turn, keep the provider connection and continue draining its
bounded event stream. Idle tool/background events belong to the runtime and
must not be attached to the next invocation.
5. Dispose or expire the idle process, confirming process-tree teardown. Preserve
4. Dispose or expire the idle process, confirming process-tree teardown. Preserve
session identity so later work can explicitly resume from provider persistence.

The process lifecycle distinguishes unprepared, queued, preparing, ready, busy,
failed, stopping and stopped. A logical runtime being acquired is not provider
readiness. Preparation errors expose delivery=not_sent. Failure after submission
preserves the existing accepted/possibly_sent semantics and must not replay a
prompt automatically.
Acquiring a logical runtime does not start a provider process or claim provider
readiness. Failure before submission remains `delivery=not_sent`; failure after
submission preserves accepted/possibly-sent semantics and never replays a prompt.

Preparation has a configurable deadline, bounded concurrency, global process
capacity and idle expiration. Active turns cannot be evicted to admit speculative
preparation. Abandoned preparations release their capacity. Disposing while
preparing prevents late successful readiness from resurrecting the runtime.
Retention has bounded turn concurrency, separate global process capacity and idle
expiration. Capacity exhaustion falls back to the ordinary cold-turn path.
Cancellation, timeout, dropped futures, disposal and ambiguous idle output poison
the connection and terminate its process tree before it can be reused.

## Configuration and credentials

Expand Down
11 changes: 11 additions & 0 deletions docs/how-to/resume-large-codex-conversations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Resume large Codex conversations

The app-server adapter requests `excludeTurns: true` when resuming a thread or
forking one after an active-writer conflict. This returns the metadata needed to
start the next turn without replaying the historical turns in one JSON frame.
Codex still uses the saved conversation context; this does not clear or truncate
history. New threads use the normal start request.

This prevents growing history replies from hitting the default 2 MiB event-line
limit. The limit remains in place for other protocol events. Clients displaying
history should fetch it separately using the provider's paginated history APIs.
32 changes: 32 additions & 0 deletions docs/reference/retained-runtimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,38 @@ reports `session_resume: true` and `retained_process: false`: Claude, Codex, and
OpenCode can continue their provider-native sessions even though the CLI is
currently relaunched for each turn.

Codex app-server reuse is explicit and in-process only:

```rust
use std::time::Duration;
use temps_agent_runtime::providers::Codex;
use temps_agent_runtime::{AgentRuntime, CodexProcessRetention};

let mut builder = AgentRuntime::builder()
.codex_process_retention(CodexProcessRetention {
max_processes: 4,
idle_timeout: Duration::from_secs(120),
});
builder.register(Codex::app_server());
let runtime = builder.build()?;
```

Pass that runtime to `InProcessRuntimeClient::new`. Each `RuntimeId` owns at most
one process. The SDK compares the complete sandbox-wrapped command plus working
directory, model, reasoning, permission, harness, launch context, compaction and
sandbox requirements before reuse. Changed explicit credentials or environment
replace the process. Ambient inherited environment is read when a process starts;
applications that rotate ambient credentials must dispose the logical runtime or
rebuild the `AgentRuntime`.

The process pool is bounded independently from acquired logical runtimes. A new
runtime that reaches capacity runs through the ordinary one-process turn path;
existing retained runtimes remain warm and usable without waiting for an idle
slot.
Any unsolicited idle frame, crash, cancellation, timeout, dropped turn future or
disposal retires the process tree. Late frames are correlated by native turn ID
and cannot enter a later invocation.

Use `RuntimeHandle::configuration_impact` before presenting a live setting
change. The compatibility driver applies per-turn model, reasoning, permission,
harness, launch-context, environment, and timeout changes live. Provider,
Expand Down
9 changes: 9 additions & 0 deletions site/src/content/docs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import architecture from "../../../docs/explanation/architecture.md?raw";
import approvals from "../../../docs/how-to/persist-approvals.md?raw";
import commandExecution from "../../../docs/how-to/persist-command-execution.md?raw";
import codexResume from "../../../docs/how-to/resume-large-codex-conversations.md?raw";
import persistence from "../../../docs/how-to/persist-conversations.md?raw";
import toolProcesses from "../../../docs/how-to/keep-tool-processes-running.md?raw";
import managedProcesses from "../../../docs/how-to/manage-background-processes.md?raw";
Expand Down Expand Up @@ -36,6 +37,14 @@ export const categories: DocCategory[] = [
];

export const docs: DocPage[] = [
{
slug: "resume-large-codex-conversations",
sourcePath: "docs/how-to/resume-large-codex-conversations.md",
title: "Resume large Codex conversations",
description: "Continue long threads without returning oversized history frames or losing provider context.",
category: "How-to guides",
body: codexResume,
},
{
slug: "quickstart",
sourcePath: "docs/tutorials/quickstart.md",
Expand Down
23 changes: 22 additions & 1 deletion src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::{
///
/// Programs and arguments remain separate values throughout execution; this
/// crate never constructs a shell command string.
#[derive(Clone)]
#[derive(Clone, PartialEq, Eq)]
pub struct CommandSpec {
/// Executable path.
pub program: PathBuf,
Expand Down Expand Up @@ -254,6 +254,14 @@ pub trait AgentAdapter: Send + Sync {
/// Provider implemented by this adapter.
fn provider(&self) -> Provider;

/// Whether this exact adapter supports retaining one native process across turns.
///
/// Custom adapters remain disabled unless they explicitly implement the
/// complete lifecycle contract.
fn supports_retained_process(&self) -> bool {
false
}

/// Executable name or path meaningful inside the selected execution transport.
fn executable(&self) -> PathBuf {
PathBuf::from(match self.provider() {
Expand Down Expand Up @@ -384,6 +392,19 @@ pub trait AgentAdapter: Send + Sync {
Ok(())
}

/// Begin another turn on an already initialized retained process.
///
/// Returning `None` means the adapter cannot safely reuse its process.
fn retained_turn_start(&self, state: &AdapterState) -> Result<Option<Vec<u8>>> {
let _ = state;
Ok(None)
}

/// Mark parser state as belonging to a retained native process.
fn mark_retained_turn(&self, state: &mut AdapterState) {
let _ = state;
}

/// Supply a protocol carrier to use instead of the child's stdout and stdin.
///
/// Called once, after the provider process is spawned and before the first
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ pub use extensions::{
HarnessMcpServer, HarnessSkill, McpServerManagementRequest, SkillManagementRequest,
};
pub use interactions::{InteractionBroker, InteractionBrokerError, InteractionResolution};
pub use runtime::{AgentRuntime, AgentRuntimeBuilder};
pub use runtime::{AgentRuntime, AgentRuntimeBuilder, CodexProcessRetention};
pub use sandbox::{
ResolvedSandboxProfile, SandboxBackend, SandboxCapabilities, SandboxContext, SandboxError,
SandboxPathAccess, SandboxProfileChange, SandboxProfileManager, SandboxProfileRef,
Expand Down
18 changes: 18 additions & 0 deletions src/providers/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,10 @@ impl AgentAdapter for Codex {
Provider::Codex
}

fn supports_retained_process(&self) -> bool {
self.app_server_mode()
}

fn executable(&self) -> PathBuf {
self.configured_executable()
}
Expand Down Expand Up @@ -1016,6 +1020,20 @@ impl AgentAdapter for Codex {
Ok(())
}

fn retained_turn_start(&self, state: &AdapterState) -> Result<Option<Vec<u8>>> {
if self.app_server_mode() {
codex_app_server::retained_turn_start(state).map(Some)
} else {
Ok(None)
}
}

fn mark_retained_turn(&self, state: &mut AdapterState) {
if self.app_server_mode() {
codex_app_server::mark_retained(state);
}
}

fn interrupt_request(&self, state: &AdapterState) -> Option<Vec<u8>> {
self.app_server_mode()
.then(|| codex_app_server::interrupt(state))
Expand Down
Loading
Loading