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-05-31 21:33:20 -07:00
committed by GitHub
Unverified
parent 3b7334d099
commit cf0911076f
92 changed files with 504 additions and 111 deletions
+1
View File
@@ -120,6 +120,7 @@ mod tests {
id: thread_id.to_string(),
session_id: thread_id.to_string(),
forked_from_id: None,
parent_thread_id: None,
preview: String::new(),
ephemeral: false,
model_provider: "openai".to_string(),
+4
View File
@@ -2767,6 +2767,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re
id: agent_thread_id.to_string(),
session_id: agent_thread_id.to_string(),
forked_from_id: None,
parent_thread_id: None,
preview: "agent thread".to_string(),
ephemeral: false,
model_provider: "agent-provider".to_string(),
@@ -2856,6 +2857,7 @@ async fn inactive_thread_started_notification_preserves_primary_model_when_path_
id: agent_thread_id.to_string(),
session_id: agent_thread_id.to_string(),
forked_from_id: None,
parent_thread_id: None,
preview: "agent thread".to_string(),
ephemeral: false,
model_provider: "agent-provider".to_string(),
@@ -2914,6 +2916,7 @@ async fn thread_read_session_state_does_not_reuse_primary_permission_profile() {
id: read_thread_id.to_string(),
session_id: read_thread_id.to_string(),
forked_from_id: None,
parent_thread_id: None,
preview: "read thread".to_string(),
ephemeral: false,
model_provider: "read-provider".to_string(),
@@ -5011,6 +5014,7 @@ async fn thread_rollback_response_discards_queued_active_thread_events() {
id: thread_id.to_string(),
session_id: thread_id.to_string(),
forked_from_id: None,
parent_thread_id: None,
preview: String::new(),
ephemeral: false,
model_provider: "openai".to_string(),
@@ -411,6 +411,7 @@ mod tests {
id: read_thread_id.to_string(),
session_id: read_thread_id.to_string(),
forked_from_id: None,
parent_thread_id: None,
preview: "read thread".to_string(),
ephemeral: false,
model_provider: "read-provider".to_string(),
+1
View File
@@ -2279,6 +2279,7 @@ mod tests {
id: thread_id.to_string(),
session_id: ThreadId::new().to_string(),
forked_from_id: Some(forked_from_id.to_string()),
parent_thread_id: None,
preview: "hello".to_string(),
ephemeral: false,
model_provider: "openai".to_string(),
+4
View File
@@ -5726,6 +5726,7 @@ session_picker_view = "dense"
id: thread_id.to_string(),
session_id: thread_id.to_string(),
forked_from_id: None,
parent_thread_id: None,
preview: String::from("remote thread"),
ephemeral: false,
model_provider: String::from("openai"),
@@ -5760,6 +5761,7 @@ session_picker_view = "dense"
id: thread_id.to_string(),
session_id: thread_id.to_string(),
forked_from_id: None,
parent_thread_id: None,
preview: String::from("preview"),
ephemeral: false,
model_provider: String::from("openai"),
@@ -5828,6 +5830,7 @@ session_picker_view = "dense"
id: thread_id.to_string(),
session_id: thread_id.to_string(),
forked_from_id: None,
parent_thread_id: None,
preview: String::from("preview"),
ephemeral: false,
model_provider: String::from("openai"),
@@ -5885,6 +5888,7 @@ session_picker_view = "dense"
id: thread_id.to_string(),
session_id: thread_id.to_string(),
forked_from_id: None,
parent_thread_id: None,
preview: String::from("preview"),
ephemeral: false,
model_provider: String::from("openai"),