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
+8
View File
@@ -1070,6 +1070,7 @@ fn session_configured_from_thread_start_response(
session_configured_from_thread_response(
&response.thread.session_id,
&response.thread.id,
response.thread.parent_thread_id.as_deref(),
response.thread.thread_source.map(Into::into),
response.thread.name.clone(),
response.thread.path.clone(),
@@ -1092,6 +1093,7 @@ fn session_configured_from_thread_resume_response(
session_configured_from_thread_response(
&response.thread.session_id,
&response.thread.id,
response.thread.parent_thread_id.as_deref(),
response.thread.thread_source.map(Into::into),
response.thread.name.clone(),
response.thread.path.clone(),
@@ -1123,6 +1125,7 @@ fn review_target_to_api(target: ReviewTarget) -> ApiReviewTarget {
fn session_configured_from_thread_response(
session_id: &str,
thread_id: &str,
parent_thread_id: Option<&str>,
thread_source: Option<codex_protocol::protocol::ThreadSource>,
thread_name: Option<String>,
rollout_path: Option<PathBuf>,
@@ -1140,11 +1143,16 @@ fn session_configured_from_thread_response(
.map_err(|err| format!("session id `{session_id}` is invalid: {err}"))?;
let thread_id = ThreadId::from_string(thread_id)
.map_err(|err| format!("thread id `{thread_id}` is invalid: {err}"))?;
let parent_thread_id = parent_thread_id
.map(ThreadId::from_string)
.transpose()
.map_err(|err| format!("parent thread id is invalid: {err}"))?;
Ok(SessionConfiguredEvent {
session_id,
thread_id,
forked_from_id: None,
parent_thread_id,
thread_source,
thread_name,
model,