Preserve rollout tracing across sub-agent reloads (#57615)

## Why

Reloading an evicted sub-agent disabled its rollout tracing, leaving later turns out of the trace. Replacement Code Mode sessions could also reuse cell IDs from earlier sessions.

## What changed

- Reattach reloaded children to the root rollout trace and emit `ThreadResumed` without duplicating `ThreadStarted`.
- Reopen existing threads during replay, preserving their identity and history while clearing stale runtime cell and terminal process mappings.
- Allocate Code Mode cell generations across sessions for both gRPC and stdio hosts to prevent ID reuse after reload.
- Record an aborted trace when session startup fails before the session loop begins.

## Testing

Add regression coverage for child eviction and reload, including a failed reload, continued inference tracing, distinct cell IDs, and preserved mail and follow-up context. Extend replay tests for history preservation and reused runtime handles, and host tests for cell IDs across session replacement and reconnection.

GitOrigin-RevId: 6c146c04b5cda9295afb7d973c0d5474b363a822
This commit is contained in:
cassirer-openai
2026-10-28 01:29:34 +00:00
committed by copyberry
parent c0e88f3c04
commit 68b67f0ff4
19 changed files with 367 additions and 100 deletions
@@ -5,6 +5,7 @@ use std::sync::Mutex;
use std::sync::PoisonError;
use std::sync::Weak;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::time::Duration;
@@ -67,6 +68,8 @@ fn inject_span_traceparent<T>(request: &mut tonic::Request<T>, span: &tracing::S
#[derive(Clone)]
pub struct GrpcCodeModeSessionProvider {
transport: Arc<SharedTransport>,
// Reloaded threads share this counter rather than reusing a cell namespace.
next_generation: Arc<AtomicU64>,
}
impl GrpcCodeModeSessionProvider {
@@ -94,6 +97,7 @@ impl GrpcCodeModeSessionProvider {
fn from_transport(transport: SharedTransport) -> Self {
Self {
transport: Arc::new(transport),
next_generation: Arc::new(AtomicU64::new(1)),
}
}
@@ -1,7 +1,6 @@
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::PoisonError;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use codex_code_mode_protocol::CodeModeSession;
@@ -34,7 +33,6 @@ struct ReconnectInner {
limits: CodeModeSessionCellExecutionLimits,
binding: Mutex<Option<SessionBinding>>,
opening_permit: Semaphore,
next_generation: AtomicU64,
shutdown_requested: CancellationToken,
shutdown_result: Mutex<Option<ShutdownResultReceiver>>,
}
@@ -56,7 +54,6 @@ impl ReconnectableSession {
limits,
binding: Mutex::new(None),
opening_permit: Semaphore::new(/*permits*/ 1),
next_generation: AtomicU64::new(1),
shutdown_requested: CancellationToken::new(),
shutdown_result: Mutex::new(None),
}),
@@ -178,7 +175,10 @@ impl ReconnectInner {
wait_for_watch(binding.session.inner.request_shutdown()).await?;
}
let generation = self.next_generation.fetch_add(1, Ordering::Relaxed);
let generation = self
.provider
.next_generation
.fetch_add(1, Ordering::Relaxed);
let session = tokio::select! {
biased;
_ = self.shutdown_requested.cancelled() => {
+7 -11
View File
@@ -187,12 +187,14 @@ impl OwnedCodeModeHost {
.cloned()
}
fn allocate_session_id(&self) -> SessionId {
let value = self.next_session_id.fetch_add(1, Ordering::Relaxed);
match SessionId::new(format!("session-{value}")) {
fn allocate_session(&self) -> RemoteSession {
// The shared ordinal also keeps cell IDs distinct after a thread reload.
let generation = self.next_session_id.fetch_add(1, Ordering::Relaxed);
let id = match SessionId::new(format!("session-{generation}")) {
Ok(session_id) => session_id,
Err(_) => unreachable!("a generated code-mode session ID is nonempty"),
}
};
RemoteSession { id, generation }
}
}
@@ -218,7 +220,6 @@ struct SessionInner {
host: Arc<OwnedCodeModeHost>,
limits: CodeModeSessionCellExecutionLimits,
state: StdMutex<SessionState>,
next_generation: AtomicU64,
shutdown_requested: AtomicBool,
shutdown_result: StdMutex<Option<ShutdownResultReceiver>>,
retired_cleanups: StdMutex<Vec<SessionCleanup>>,
@@ -245,7 +246,6 @@ impl ProcessOwnedCodeModeSession {
host,
limits,
state: StdMutex::new(SessionState::New),
next_generation: AtomicU64::new(1),
shutdown_requested: AtomicBool::new(false),
shutdown_result: StdMutex::new(None),
retired_cleanups: StdMutex::new(Vec::new()),
@@ -312,11 +312,7 @@ impl SessionInner {
.unwrap_or_else(std::sync::PoisonError::into_inner);
match &*state {
SessionState::New => {
let generation = self.next_generation.fetch_add(1, Ordering::Relaxed);
let remote = RemoteSession {
id: self.host.allocate_session_id(),
generation,
};
let remote = self.host.allocate_session();
let (result_tx, result_rx) = watch::channel(None);
*state = SessionState::Opening {
remote: remote.clone(),