diff --git a/codex-rs/app-server/tests/suite/v2/external_agent_import_sync.rs b/codex-rs/app-server/tests/suite/v2/external_agent_import_sync.rs index 4142127303..a527e45d41 100644 --- a/codex-rs/app-server/tests/suite/v2/external_agent_import_sync.rs +++ b/codex-rs/app-server/tests/suite/v2/external_agent_import_sync.rs @@ -246,6 +246,31 @@ fn assert_history(response: &ThreadReadResponse, expected: &[(&str, &str)]) { assert_eq!(markers, 1, "the import marker must appear exactly once"); } +#[tokio::test] +async fn backgrounded_session_imports_only_its_continuation() -> Result<()> { + let mut fixture = ImportFixture::new().await?; + let continuation = fixture.session_path.with_file_name("background.jsonl"); + std::fs::copy(&fixture.session_path, &continuation)?; + fixture.append_to(&continuation, &[("assistant", LATE_ASSISTANT)])?; + fixture.append_raw(&format!( + "{}\n", + json!({ + "type": "continued-in", + "sessionId": "session", + "continuedInSessionId": "background", + }) + ))?; + + let imported = fixture.import_one().await?; + assert_eq!(fixture.thread_count().await?, 1); + assert_history( + &fixture.read(&imported).await?, + &[("user", FIRST_USER), ("assistant", LATE_ASSISTANT)], + ); + assert!(fixture.detect().await?.items.is_empty()); + Ok(()) +} + #[tokio::test] async fn cold_exact_prefix_appends_suffix_to_same_task_and_checkpoints() -> Result<()> { let mut fixture = ImportFixture::new().await?; diff --git a/codex-rs/external-agent-migration/src/detect/sessions/cla.rs b/codex-rs/external-agent-migration/src/detect/sessions/cla.rs index 9ff44b2c6a..72e86461a7 100644 --- a/codex-rs/external-agent-migration/src/detect/sessions/cla.rs +++ b/codex-rs/external-agent-migration/src/detect/sessions/cla.rs @@ -65,6 +65,7 @@ mod tests { use super::*; use crate::sessions::ledger::record_imported_session; use codex_protocol::ThreadId; + use pretty_assertions::assert_eq; use serde_json::Value as JsonValue; use std::fs::FileTimes; use std::fs::OpenOptions; @@ -101,6 +102,45 @@ mod tests { ); } + #[test] + fn detects_only_the_continuation_of_a_backgrounded_session() { + let root = TempDir::new().expect("tempdir"); + let external_agent_home = root.path().join(".external"); + let project_root = root.path().join("repo"); + let original = record("user", "plan a vacation", &project_root); + write_session( + &external_agent_home, + &project_root, + "original.jsonl", + &[ + original.clone(), + serde_json::json!({ + "type": "continued-in", + "sessionId": "original", + "continuedInSessionId": "background", + }), + ], + ); + let continuation = write_session( + &external_agent_home, + &project_root, + "background.jsonl", + &[ + original, + record("assistant", "Here is your itinerary", &project_root), + ], + ); + + assert_eq!( + detect_recent_cla_sessions(&external_agent_home, root.path()).expect("detect"), + vec![ExternalAgentSessionMigration { + path: continuation, + cwd: project_root, + title: Some("plan a vacation".to_string()), + }] + ); + } + #[test] fn prefers_latest_custom_title_over_first_user_message() { let root = TempDir::new().expect("tempdir"); diff --git a/codex-rs/external-agent-migration/src/sessions/records_cla.rs b/codex-rs/external-agent-migration/src/sessions/records_cla.rs index 13ff14cd70..957995074c 100644 --- a/codex-rs/external-agent-migration/src/sessions/records_cla.rs +++ b/codex-rs/external-agent-migration/src/sessions/records_cla.rs @@ -40,6 +40,15 @@ pub fn summarize_session(path: &Path) -> io::Result> { let Ok(mut record) = serde_json::from_str::(trimmed) else { continue; }; + // Backgrounding replaces this session with a continuation that includes its history. + if record.get("type").and_then(JsonValue::as_str) == Some("continued-in") + && record + .get("continuedInSessionId") + .and_then(JsonValue::as_str) + .is_some_and(|id| !id.trim().is_empty()) + { + return Ok(None); + } if cwd.is_none() { cwd = record .get("cwd")