Fix fork source display in /status (expose forked_from_id in app server) (#16596)

Addresses #16560

Problem: `/status` stopped showing the source thread id in forked TUI
sessions after the app-server migration.

Solution: Carry fork source ids through app-server v2 thread data and
the TUI session adapter, and update TUI fixtures so `/status` matches
the old TUI behavior.
This commit is contained in:
Eric Traut
2026-04-02 15:05:29 -06:00
committed by GitHub
Unverified
parent 93380a6fac
commit 9bb7f0a694
27 changed files with 267 additions and 3 deletions
+3
View File
@@ -8639,6 +8639,7 @@ guardian_approval = true
ServerNotification::ThreadStarted(ThreadStartedNotification {
thread: Thread {
id: agent_thread_id.to_string(),
forked_from_id: None,
preview: "agent thread".to_string(),
ephemeral: false,
model_provider: "agent-provider".to_string(),
@@ -8719,6 +8720,7 @@ guardian_approval = true
ServerNotification::ThreadStarted(ThreadStartedNotification {
thread: Thread {
id: agent_thread_id.to_string(),
forked_from_id: None,
preview: "agent thread".to_string(),
ephemeral: false,
model_provider: "agent-provider".to_string(),
@@ -10680,6 +10682,7 @@ guardian_approval = true
&ThreadRollbackResponse {
thread: Thread {
id: thread_id.to_string(),
forked_from_id: None,
preview: String::new(),
ephemeral: false,
model_provider: "openai".to_string(),
@@ -1251,6 +1251,7 @@ mod tests {
fn replays_command_execution_items_from_thread_snapshots() {
let thread = Thread {
id: "019cee8c-b993-7e33-88c0-014d4e62612d".to_string(),
forked_from_id: None,
preview: String::new(),
ephemeral: false,
model_provider: "openai".to_string(),
@@ -1417,6 +1418,7 @@ mod tests {
let events = thread_snapshot_events(
&Thread {
id: thread_id.to_string(),
forked_from_id: None,
preview: "hello".to_string(),
ephemeral: false,
model_provider: "openai".to_string(),
+1
View File
@@ -111,6 +111,7 @@ mod tests {
fn test_thread(thread_id: ThreadId, source: SessionSource) -> Thread {
Thread {
id: thread_id.to_string(),
forked_from_id: None,
preview: String::new(),
ephemeral: false,
model_provider: "openai".to_string(),
+42 -1
View File
@@ -945,6 +945,7 @@ async fn thread_session_state_from_thread_start_response(
) -> Result<ThreadSessionState, String> {
thread_session_state_from_thread_response(
&response.thread.id,
response.thread.forked_from_id.clone(),
response.thread.name.clone(),
response.thread.path.clone(),
response.model.clone(),
@@ -966,6 +967,7 @@ async fn thread_session_state_from_thread_resume_response(
) -> Result<ThreadSessionState, String> {
thread_session_state_from_thread_response(
&response.thread.id,
response.thread.forked_from_id.clone(),
response.thread.name.clone(),
response.thread.path.clone(),
response.model.clone(),
@@ -987,6 +989,7 @@ async fn thread_session_state_from_thread_fork_response(
) -> Result<ThreadSessionState, String> {
thread_session_state_from_thread_response(
&response.thread.id,
response.thread.forked_from_id.clone(),
response.thread.name.clone(),
response.thread.path.clone(),
response.model.clone(),
@@ -1027,6 +1030,7 @@ fn review_target_to_app_server(
)]
async fn thread_session_state_from_thread_response(
thread_id: &str,
forked_from_id: Option<String>,
thread_name: Option<String>,
rollout_path: Option<PathBuf>,
model: String,
@@ -1041,12 +1045,17 @@ async fn thread_session_state_from_thread_response(
) -> Result<ThreadSessionState, String> {
let thread_id = ThreadId::from_string(thread_id)
.map_err(|err| format!("thread id `{thread_id}` is invalid: {err}"))?;
let forked_from_id = forked_from_id
.as_deref()
.map(ThreadId::from_string)
.transpose()
.map_err(|err| format!("forked_from_id is invalid: {err}"))?;
let (history_log_id, history_entry_count) = message_history::history_metadata(config).await;
let history_entry_count = u64::try_from(history_entry_count).unwrap_or(u64::MAX);
Ok(ThreadSessionState {
thread_id,
forked_from_id: None,
forked_from_id,
thread_name,
model,
model_provider_id,
@@ -1164,9 +1173,11 @@ mod tests {
let temp_dir = tempfile::tempdir().expect("tempdir");
let config = build_config(&temp_dir).await;
let thread_id = ThreadId::new();
let forked_from_id = ThreadId::new();
let response = ThreadResumeResponse {
thread: codex_app_server_protocol::Thread {
id: thread_id.to_string(),
forked_from_id: Some(forked_from_id.to_string()),
preview: "hello".to_string(),
ephemeral: false,
model_provider: "openai".to_string(),
@@ -1215,6 +1226,7 @@ mod tests {
let started = started_thread_from_resume_response(response.clone(), &config)
.await
.expect("resume response should map");
assert_eq!(started.session.forked_from_id, Some(forked_from_id));
assert_eq!(started.turns.len(), 1);
assert_eq!(started.turns[0], response.thread.turns[0]);
}
@@ -1234,6 +1246,7 @@ mod tests {
let session = thread_session_state_from_thread_response(
&thread_id.to_string(),
/*forked_from_id*/ None,
Some("restore".to_string()),
/*rollout_path*/ None,
"gpt-5.4".to_string(),
@@ -1253,6 +1266,34 @@ mod tests {
assert_eq!(session.history_entry_count, 2);
}
#[tokio::test]
async fn session_configured_preserves_fork_source_thread_id() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let config = build_config(&temp_dir).await;
let thread_id = ThreadId::new();
let forked_from_id = ThreadId::new();
let session = thread_session_state_from_thread_response(
&thread_id.to_string(),
Some(forked_from_id.to_string()),
Some("restore".to_string()),
/*rollout_path*/ None,
"gpt-5.4".to_string(),
"openai".to_string(),
/*service_tier*/ None,
AskForApproval::Never,
codex_protocol::config_types::ApprovalsReviewer::User,
SandboxPolicy::new_read_only_policy(),
PathBuf::from("/tmp/project"),
/*reasoning_effort*/ None,
&config,
)
.await
.expect("session should map");
assert_eq!(session.forked_from_id, Some(forked_from_id));
}
#[test]
fn status_account_display_from_auth_mode_uses_remapped_plan_labels() {
let business = status_account_display_from_auth_mode(
+1
View File
@@ -2639,6 +2639,7 @@ mod tests {
let thread_id = ThreadId::new();
let thread = Thread {
id: thread_id.to_string(),
forked_from_id: None,
preview: String::from("remote thread"),
ephemeral: false,
model_provider: String::from("openai"),