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 authored and GitHub committed 2026-06-01 04:33:20 +00:00
1 parent 3b7334d099
commit cf0911076f
92 files changed
+504 -111

No files matched your search

+10
View File
@@ -81,6 +81,7 @@ pub enum RolloutRecorderParams {
Create {
conversation_id: ThreadId,
forked_from_id: Option<ThreadId>,
parent_thread_id: Option<ThreadId>,
source: SessionSource,
thread_source: Option<ThreadSource>,
base_instructions: BaseInstructions,
@@ -156,6 +157,7 @@ impl RolloutRecorderParams {
pub fn new(
conversation_id: ThreadId,
forked_from_id: Option<ThreadId>,
parent_thread_id: Option<ThreadId>,
source: SessionSource,
thread_source: Option<ThreadSource>,
base_instructions: BaseInstructions,
@@ -164,6 +166,7 @@ impl RolloutRecorderParams {
Self::Create {
conversation_id,
forked_from_id,
parent_thread_id,
source,
thread_source,
base_instructions,
@@ -652,6 +655,7 @@ impl RolloutRecorder {
RolloutRecorderParams::Create {
conversation_id,
forked_from_id,
parent_thread_id,
source,
thread_source,
base_instructions,
@@ -673,6 +677,7 @@ impl RolloutRecorder {
let session_meta = SessionMeta {
id: session_id,
forked_from_id,
parent_thread_id,
timestamp,
cwd: config.cwd().to_path_buf(),
originator: originator().value,
@@ -1020,6 +1025,7 @@ fn fill_missing_thread_item_metadata(item: &mut ThreadItem, state_item: ThreadIt
git_sha,
git_origin_url,
source,
parent_thread_id,
agent_nickname,
agent_role,
model_provider,
@@ -1049,6 +1055,9 @@ fn fill_missing_thread_item_metadata(item: &mut ThreadItem, state_item: ThreadIt
if item.source.is_none() {
item.source = source;
}
if item.parent_thread_id.is_none() {
item.parent_thread_id = parent_thread_id;
}
if item.agent_nickname.is_none() {
item.agent_nickname = agent_nickname;
}
@@ -1690,6 +1699,7 @@ fn thread_item_from_state_metadata(item: codex_state::ThreadMetadata) -> ThreadI
.or_else(|_| serde_json::from_value(Value::String(item.source)))
.unwrap_or(SessionSource::Unknown),
),
parent_thread_id: None,
agent_nickname: item.agent_nickname,
agent_role: item.agent_role,
model_provider: Some(item.model_provider),