Persist session IDs across thread resume (#29327)

## Summary

A cold-resumed subagent kept its durable thread ID but could receive a
new session ID, splitting one agent tree across multiple sessions after
a restart.

Persist the root session ID in every rollout `SessionMeta`, carry it
through thread creation, and restore it before initializing the resumed
`Session` and `AgentControl`.

## Behavior

For a nested agent tree:

```text
root session R
  parent thread P
    child thread C
```

The child rollout stores:

```text
session_id:       R
parent_thread_id: P
id:               C
```

After a cold resume, the child still belongs to root session `R` while
its immediate parent remains `P`. The integration coverage uses distinct
values for all three IDs so it catches restoring the session from
`parent_thread_id`.

## Legacy rollouts

Previous rollouts have `id` but no `session_id`. `SessionMetaLine`
deserialization treats a missing `session_id` as `id`, keeping those
files readable, listable, and resumable. When a legacy subagent is
resumed through its root, that synthesized child ID no longer overrides
the inherited root-scoped `AgentControl`. New rollouts always persist
the explicit root session ID.
This commit is contained in:
jif
2026-06-22 09:36:08 +02:00
committed by GitHub
parent 98845e4840
commit 6d15bb3d17
38 changed files with 193 additions and 34 deletions
+9 -1
View File
@@ -1,4 +1,5 @@
use anyhow::Result;
use codex_protocol::SessionId;
use codex_protocol::ThreadId;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::GitInfo;
@@ -127,11 +128,12 @@ pub fn create_fake_rollout_with_source(
model_provider,
git_info,
source,
/*session_id*/ None,
/*parent_thread_id*/ None,
)
}
/// Create a minimal rollout file with an explicit session source and control parent.
/// Create a minimal rollout file with an explicit root session and control parent.
#[allow(clippy::too_many_arguments)]
pub fn create_fake_parented_rollout_with_source(
codex_home: &Path,
@@ -141,6 +143,7 @@ pub fn create_fake_parented_rollout_with_source(
model_provider: Option<&str>,
git_info: Option<GitInfo>,
source: SessionSource,
session_id: SessionId,
parent_thread_id: ThreadId,
) -> Result<String> {
create_fake_rollout_with_source_and_parent_thread_id(
@@ -151,6 +154,7 @@ pub fn create_fake_parented_rollout_with_source(
model_provider,
git_info,
source,
Some(session_id),
Some(parent_thread_id),
)
}
@@ -164,11 +168,13 @@ fn create_fake_rollout_with_source_and_parent_thread_id(
model_provider: Option<&str>,
git_info: Option<GitInfo>,
source: SessionSource,
session_id: Option<SessionId>,
parent_thread_id: Option<ThreadId>,
) -> Result<String> {
let uuid = Uuid::new_v4();
let uuid_str = uuid.to_string();
let conversation_id = ThreadId::from_string(&uuid_str)?;
let session_id = session_id.unwrap_or_else(|| conversation_id.into());
let file_path = rollout_path(codex_home, filename_ts, &uuid_str);
let dir = file_path
@@ -178,6 +184,7 @@ fn create_fake_rollout_with_source_and_parent_thread_id(
// Build JSONL lines
let meta = SessionMeta {
session_id,
id: conversation_id,
forked_from_id: None,
parent_thread_id,
@@ -264,6 +271,7 @@ pub fn create_fake_rollout_with_text_elements(
// Build JSONL lines
let meta = SessionMeta {
session_id: conversation_id.into(),
id: conversation_id,
forked_from_id: None,
parent_thread_id: None,