store and expose parent_thread_id on Threads (#25113)

## Why

This PR
https://github.com/openai/codex/pull/24161#discussion_r3325692763
revealed a subagent data modeling issue, where we overloaded
`forked_from_id` to also mean `parent_thread_id`. That's incorrect since
guardian and review subagents can be a subagent and NOT fork the main
thread's history.

The solution here is to explicitly store a new `parent_thread_id` on
`SessionMeta`, alongside `forked_from_id` which already exists. While
we're at it, also expose it in the app-server protocol on the `Thread`
object.

A thread->subagent relationship and a fork of thread history are
orthogonal concepts.

## What Changed

- Added top-level `parent_thread_id` persistence on `SessionMeta` and
runtime/session plumbing through `SessionConfiguredEvent`,
`CodexSpawnArgs`, `SessionConfiguration`, `ThreadConfigSnapshot`,
`TurnContext`, and `ModelClient`.
- Made turn metadata, request headers, analytics, and subagent-start
events read the separate runtime/top-level parent field instead of
deriving general parent lineage from `SessionSource` or
`forked_from_thread_id`.
- Passed parent lineage separately at delegated subagent, review,
guardian, agent-job, and multi-agent spawn construction sites;
copied-history fork lineage remains derived only from `InitialHistory`.
- Persisted and exposed parent lineage through rollout/thread-store
projections and app-server v2 `Thread.parentThreadId`.
- Updated app-server README text and regenerated app-server schema
fixtures for the additive `parentThreadId` response field.
This commit is contained in:
Owen Lin
2026-06-01 04:33:20 +00:00
committed by GitHub
parent 3b7334d099
commit cf0911076f
92 changed files with 504 additions and 111 deletions
+1
View File
@@ -38,6 +38,7 @@ pub use responses::create_final_assistant_message_sse_response;
pub use responses::create_request_permissions_sse_response;
pub use responses::create_request_user_input_sse_response;
pub use responses::create_shell_command_sse_response;
pub use rollout::create_fake_parented_rollout_with_source;
pub use rollout::create_fake_rollout;
pub use rollout::create_fake_rollout_with_source;
pub use rollout::create_fake_rollout_with_text_elements;
@@ -118,6 +118,53 @@ pub fn create_fake_rollout_with_source(
model_provider: Option<&str>,
git_info: Option<GitInfo>,
source: SessionSource,
) -> Result<String> {
create_fake_rollout_with_source_and_parent_thread_id(
codex_home,
filename_ts,
meta_rfc3339,
preview,
model_provider,
git_info,
source,
/*parent_thread_id*/ None,
)
}
/// Create a minimal rollout file with an explicit session source and control parent.
#[allow(clippy::too_many_arguments)]
pub fn create_fake_parented_rollout_with_source(
codex_home: &Path,
filename_ts: &str,
meta_rfc3339: &str,
preview: &str,
model_provider: Option<&str>,
git_info: Option<GitInfo>,
source: SessionSource,
parent_thread_id: ThreadId,
) -> Result<String> {
create_fake_rollout_with_source_and_parent_thread_id(
codex_home,
filename_ts,
meta_rfc3339,
preview,
model_provider,
git_info,
source,
Some(parent_thread_id),
)
}
#[allow(clippy::too_many_arguments)]
fn create_fake_rollout_with_source_and_parent_thread_id(
codex_home: &Path,
filename_ts: &str,
meta_rfc3339: &str,
preview: &str,
model_provider: Option<&str>,
git_info: Option<GitInfo>,
source: SessionSource,
parent_thread_id: Option<ThreadId>,
) -> Result<String> {
let uuid = Uuid::new_v4();
let uuid_str = uuid.to_string();
@@ -133,6 +180,7 @@ pub fn create_fake_rollout_with_source(
let meta = SessionMeta {
id: conversation_id,
forked_from_id: None,
parent_thread_id,
timestamp: meta_rfc3339.to_string(),
cwd: PathBuf::from("/"),
originator: "codex".to_string(),
@@ -217,6 +265,7 @@ pub fn create_fake_rollout_with_text_elements(
let meta = SessionMeta {
id: conversation_id,
forked_from_id: None,
parent_thread_id: None,
timestamp: meta_rfc3339.to_string(),
cwd: PathBuf::from("/"),
originator: "codex".to_string(),