mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
3b7334d099
commit
cf0911076f
@@ -211,6 +211,7 @@ async fn config_summary_entries_include_runtime_workspace_roots() {
|
||||
session_id: SessionId::new(),
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
thread_name: None,
|
||||
model: "gpt-5.4".to_string(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -307,6 +307,7 @@ fn turn_items_for_thread_returns_matching_turn_items() {
|
||||
id: "thread-1".to_string(),
|
||||
session_id: "thread-1".to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: String::new(),
|
||||
ephemeral: false,
|
||||
model_provider: "openai".to_string(),
|
||||
@@ -585,12 +586,33 @@ async fn session_configured_from_thread_response_preserves_thread_source() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_configured_from_thread_response_preserves_parent_thread_id() {
|
||||
let codex_home = tempdir().expect("create temp codex home");
|
||||
let cwd = tempdir().expect("create temp cwd");
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(cwd.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("build config");
|
||||
let parent_thread_id = ThreadId::new();
|
||||
let mut response = sample_thread_start_response();
|
||||
response.thread.parent_thread_id = Some(parent_thread_id.to_string());
|
||||
|
||||
let event = session_configured_from_thread_start_response(&response, &config)
|
||||
.expect("build bootstrap session configured event");
|
||||
|
||||
assert_eq!(event.parent_thread_id, Some(parent_thread_id));
|
||||
}
|
||||
|
||||
fn sample_thread_start_response() -> ThreadStartResponse {
|
||||
ThreadStartResponse {
|
||||
thread: codex_app_server_protocol::Thread {
|
||||
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
||||
session_id: "67e55044-10b1-426f-9247-bb680e5fe0c7".to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
preview: String::new(),
|
||||
ephemeral: false,
|
||||
model_provider: "openai".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user