Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ impl ExternalAgentSessionImporter {
};
let now = Utc::now();
let create_params = CreateThreadParams {
session_id: thread_id.into(),
thread_id,
extra_config: None,
forked_from_id: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,7 @@ mod thread_processor_behavior_tests {
let timestamp = "2025-09-05T16:53:11.850Z".to_string();

let session_meta = SessionMeta {
session_id: conversation_id.into(),
id: conversation_id,
timestamp: timestamp.clone(),
model_provider: None,
Expand Down Expand Up @@ -1060,6 +1061,7 @@ mod thread_processor_behavior_tests {
let timestamp = "2025-09-05T16:53:11.850Z".to_string();

let session_meta = SessionMeta {
session_id: parent_thread_id.into(),
id: conversation_id,
timestamp: timestamp.clone(),
source: SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
Expand Down Expand Up @@ -1110,6 +1112,7 @@ mod thread_processor_behavior_tests {
let timestamp = "2025-09-05T16:53:11.850Z".to_string();

let session_meta = SessionMeta {
session_id: conversation_id.into(),
id: conversation_id,
forked_from_id: Some(forked_from_id),
timestamp: timestamp.clone(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ fn extract_conversation_summary_prefers_plain_user_messages() -> Result<()> {

let head = vec![
json!({
"session_id": conversation_id.to_string(),
"id": conversation_id.to_string(),
"timestamp": timestamp,
"cwd": "/",
Expand Down
10 changes: 9 additions & 1 deletion codex-rs/app-server/tests/common/rollout.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::Result;
use codex_protocol::SessionId;
use codex_protocol::ThreadId;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::GitInfo;
Expand Down Expand Up @@ -127,11 +128,12 @@ pub fn create_fake_rollout_with_source(
model_provider,
git_info,
source,
/*session_id*/ None,
/*parent_thread_id*/ None,
)
}

/// Create a minimal rollout file with an explicit session source and control parent.
/// Create a minimal rollout file with an explicit root session and control parent.
#[allow(clippy::too_many_arguments)]
pub fn create_fake_parented_rollout_with_source(
codex_home: &Path,
Expand All @@ -141,6 +143,7 @@ pub fn create_fake_parented_rollout_with_source(
model_provider: Option<&str>,
git_info: Option<GitInfo>,
source: SessionSource,
session_id: SessionId,
parent_thread_id: ThreadId,
) -> Result<String> {
create_fake_rollout_with_source_and_parent_thread_id(
Expand All @@ -151,6 +154,7 @@ pub fn create_fake_parented_rollout_with_source(
model_provider,
git_info,
source,
Some(session_id),
Some(parent_thread_id),
)
}
Expand All @@ -164,11 +168,13 @@ fn create_fake_rollout_with_source_and_parent_thread_id(
model_provider: Option<&str>,
git_info: Option<GitInfo>,
source: SessionSource,
session_id: Option<SessionId>,
parent_thread_id: Option<ThreadId>,
) -> Result<String> {
let uuid = Uuid::new_v4();
let uuid_str = uuid.to_string();
let conversation_id = ThreadId::from_string(&uuid_str)?;
let session_id = session_id.unwrap_or_else(|| conversation_id.into());

let file_path = rollout_path(codex_home, filename_ts, &uuid_str);
let dir = file_path
Expand All @@ -178,6 +184,7 @@ fn create_fake_rollout_with_source_and_parent_thread_id(

// Build JSONL lines
let meta = SessionMeta {
session_id,
id: conversation_id,
forked_from_id: None,
parent_thread_id,
Expand Down Expand Up @@ -264,6 +271,7 @@ pub fn create_fake_rollout_with_text_elements(

// Build JSONL lines
let meta = SessionMeta {
session_id: conversation_id.into(),
id: conversation_id,
forked_from_id: None,
parent_thread_id: None,
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/conversation_summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() ->
let thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000125")?;
store
.create_thread(CreateThreadParams {
session_id: thread_id.into(),
thread_id,
extra_config: None,
forked_from_id: None,
Expand Down
10 changes: 9 additions & 1 deletion codex-rs/app-server/tests/suite/v2/client_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ async fn review_start_sends_parent_lineage_in_turn_metadata_for_thread_fork_v2()
}

#[tokio::test]
async fn turn_start_sends_other_subagent_lineage_after_cold_thread_resume_v2() -> Result<()> {
async fn turn_start_sends_nested_subagent_lineage_after_cold_thread_resume_v2() -> Result<()> {
skip_if_no_network!(Ok(()));

let server = responses::start_mock_server().await;
Expand All @@ -321,6 +321,8 @@ async fn turn_start_sends_other_subagent_lineage_after_cold_thread_resume_v2() -
/*supports_websockets*/ false,
)?;

let root_thread_id = CoreThreadId::new();
let root_thread_id_str = root_thread_id.to_string();
let parent_thread_id = CoreThreadId::new();
let parent_thread_id_str = parent_thread_id.to_string();
let subagent_thread_id = create_fake_parented_rollout_with_source(
Expand All @@ -331,6 +333,7 @@ async fn turn_start_sends_other_subagent_lineage_after_cold_thread_resume_v2() -
Some("mock_provider"),
/*git_info*/ None,
SessionSource::SubAgent(SubAgentSource::Other("guardian".to_string())),
root_thread_id.into(),
parent_thread_id,
)?;

Expand All @@ -350,6 +353,7 @@ async fn turn_start_sends_other_subagent_lineage_after_cold_thread_resume_v2() -
.await??;
let ThreadResumeResponse { thread, .. } = to_response::<ThreadResumeResponse>(resume_resp)?;
assert_eq!(thread.id, subagent_thread_id);
assert_eq!(thread.session_id, root_thread_id_str);
assert_eq!(thread.parent_thread_id, Some(parent_thread_id_str.clone()));
assert_eq!(
thread.source,
Expand Down Expand Up @@ -390,6 +394,10 @@ async fn turn_start_sends_other_subagent_lineage_after_cold_thread_resume_v2() -
Some(parent_thread_id_str.as_str())
);
assert_eq!(metadata["subagent_kind"].as_str(), Some("guardian"));
assert_eq!(
metadata["session_id"].as_str(),
Some(thread.session_id.as_str())
);
assert_eq!(metadata["thread_id"].as_str(), Some(thread.id.as_str()));
assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str()));
assert!(metadata.get("forked_from_thread_id").is_none());
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/v2/remote_thread_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ async fn thread_delete_with_non_local_thread_store_does_not_create_local_persist
let unloaded_thread_id = ThreadId::from_string(&Uuid::new_v4().to_string())?;
thread_store
.create_thread(StoreCreateThreadParams {
session_id: unloaded_thread_id.into(),
thread_id: unloaded_thread_id,
extra_config: None,
forked_from_id: None,
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/v2/thread_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,7 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> {
Some("mock_provider"),
/*git_info*/ None,
CoreSessionSource::SubAgent(SubAgentSource::Review),
parent_thread_id.into(),
parent_thread_id,
)?;
let compact_id = create_fake_rollout_with_source(
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/v2/thread_read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,7 @@ async fn seed_pathless_store_thread(
) -> Result<()> {
store
.create_thread(CreateThreadParams {
session_id: thread_id.into(),
thread_id,
extra_config: None,
forked_from_id: None,
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/app-server/tests/suite/v2/thread_resume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2054,6 +2054,7 @@ stream_max_retries = 0
let rollout_dir = rollout_path.parent().expect("rollout parent directory");
std::fs::create_dir_all(rollout_dir)?;
let session_meta = SessionMeta {
session_id: conversation_id.into(),
id: conversation_id,
forked_from_id: None,
parent_thread_id: None,
Expand Down Expand Up @@ -2735,6 +2736,7 @@ async fn thread_resume_rejects_mismatched_path_for_running_thread_id() -> Result
"timestamp": "2025-01-01T00:00:00Z",
"type": "session_meta",
"payload": {
"session_id": thread_uuid,
"id": thread_uuid,
"timestamp": "2025-01-01T00:00:00Z",
"cwd": codex_home.path(),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/v2/thread_unarchive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> {
let parent_thread_id = ThreadId::from_string("00000000-0000-4000-8000-000000000127")?;
store
.create_thread(CreateThreadParams {
session_id: thread_id.into(),
thread_id,
extra_config: None,
forked_from_id: Some(parent_thread_id),
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/cli/src/doctor/thread_inventory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -816,11 +816,13 @@ mod tests {
};
std::fs::create_dir_all(&root).expect("rollout dir");
let path = root.join(format!("rollout-{timestamp}-{thread_id}.jsonl"));
let parsed_thread_id = ThreadId::from_string(thread_id).expect("thread id");
let rollout_line = RolloutLine {
timestamp: timestamp.to_string(),
item: RolloutItem::SessionMeta(codex_protocol::protocol::SessionMetaLine {
meta: codex_protocol::protocol::SessionMeta {
id: ThreadId::from_string(thread_id).expect("thread id"),
session_id: parsed_thread_id.into(),
id: parsed_thread_id,
timestamp: timestamp.to_string(),
cwd: self.codex_home.path().to_path_buf(),
originator: "test".to_string(),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/personality_migration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R

let session_meta = SessionMetaLine {
meta: SessionMeta {
session_id: thread_id.into(),
id: thread_id,
forked_from_id: None,
parent_thread_id: None,
Expand Down
39 changes: 28 additions & 11 deletions codex-rs/core/src/session/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,33 @@ impl Session {
}
InitialHistory::Resumed(resumed_history) => resumed_history.conversation_id,
};
let resumed_session_id = match &initial_history {
InitialHistory::Resumed(resumed) => {
resumed.history.iter().find_map(|item| match item {
RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.session_id),
Comment thread
jif-oai marked this conversation as resolved.
_ => None,
})
}
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => None,
};
// Legacy subagent rollouts synthesize session_id from their own thread id.
let resumed_session_id = resumed_session_id.filter(|session_id| {
!session_configuration.session_source.is_non_root_agent()
|| *session_id != SessionId::from(thread_id)
});
let session_id = resumed_session_id.unwrap_or_else(|| {
if session_configuration.session_source.is_non_root_agent() {
agent_control.session_id()
} else {
SessionId::from(thread_id)
}
});
let agent_control = agent_control.with_session_id(
session_id,
config
.effective_agent_max_threads(MultiAgentVersion::V2)
.unwrap_or(usize::MAX),
);
let time_provider = crate::current_time::resolve_time_provider(
config.current_time_reminder.as_ref(),
external_time_provider,
Expand All @@ -546,6 +573,7 @@ impl Session {
let live_thread = match &initial_history {
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => {
let params = CreateThreadParams {
session_id,
thread_id,
extra_config: config.extra_config.clone(),
forked_from_id,
Expand Down Expand Up @@ -952,17 +980,6 @@ impl Session {
config.analytics_enabled,
)
});
let session_id = if session_configuration.session_source.is_non_root_agent() {
agent_control.session_id()
} else {
SessionId::from(thread_id)
};
let agent_control = agent_control.with_session_id(
session_id,
config
.effective_agent_max_threads(MultiAgentVersion::V2)
.unwrap_or(usize::MAX),
);
// Keep one stable manager handle for the session so extension resource clients
// automatically observe the manager installed at startup and on later refreshes.
let mcp_connection_manager = Arc::new(arc_swap::ArcSwap::from_pointee(
Expand Down
17 changes: 14 additions & 3 deletions codex-rs/core/src/session/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1900,6 +1900,7 @@ fn session_meta_item(
) -> RolloutItem {
RolloutItem::SessionMeta(SessionMetaLine {
meta: SessionMeta {
session_id: thread_id.into(),
id: thread_id,
multi_agent_version,
..SessionMeta::default()
Expand Down Expand Up @@ -3750,6 +3751,7 @@ async fn attach_thread_persistence(session: &mut Session) -> PathBuf {
let live_thread = LiveThread::create(
Arc::clone(&session.services.thread_store),
CreateThreadParams {
session_id: session.session_id(),
thread_id: session.thread_id,
extra_config: None,
forked_from_id: None,
Expand Down Expand Up @@ -5453,7 +5455,7 @@ async fn resumed_root_session_uses_thread_id_as_session_id() {
}

#[tokio::test]
async fn resumed_subagent_session_keeps_inherited_session_id() {
async fn resumed_subagent_session_restores_persisted_session_id() {
let parent_thread_id = ThreadId::new();
let parent_session_id = SessionId::from(parent_thread_id);
let thread_id = ThreadId::new();
Expand All @@ -5467,11 +5469,19 @@ async fn resumed_subagent_session_keeps_inherited_session_id() {
let (session, rx_event) = make_session_with_history_source_and_agent_control_and_rx(
InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history: Vec::new(),
history: vec![RolloutItem::SessionMeta(SessionMetaLine {
meta: SessionMeta {
session_id: parent_session_id,
id: thread_id,
source: session_source.clone(),
..SessionMeta::default()
},
git: None,
})],
rollout_path: None,
}),
session_source,
AgentControl::default().with_session_id(parent_session_id, /*max_threads*/ usize::MAX),
AgentControl::default(),
)
.await
.expect("resume should succeed");
Expand Down Expand Up @@ -6590,6 +6600,7 @@ async fn shutdown_complete_does_not_append_to_thread_store_after_shutdown() {
let live_thread = LiveThread::create(
Arc::clone(&thread_store),
CreateThreadParams {
session_id: session.session_id(),
thread_id: session.thread_id,
extra_config: None,
forked_from_id: None,
Expand Down
9 changes: 7 additions & 2 deletions codex-rs/core/tests/suite/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
"timestamp": "2024-01-01T00:00:00.000Z",
"type": "session_meta",
"payload": {
"session_id": convo_id,
"id": convo_id,
"timestamp": "2024-01-01T00:00:00Z",
"instructions": "be nice",
Expand Down Expand Up @@ -726,12 +727,14 @@ async fn resume_replays_legacy_js_repl_image_rollout_shapes() {
metadata: None,
};
let legacy_image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
let thread_id = ThreadId::default();
let rollout = vec![
RolloutLine {
timestamp: "2024-01-01T00:00:00.000Z".to_string(),
item: RolloutItem::SessionMeta(SessionMetaLine {
meta: SessionMeta {
id: ThreadId::default(),
session_id: thread_id.into(),
id: thread_id,
parent_thread_id: None,
timestamp: "2024-01-01T00:00:00Z".to_string(),
cwd: ".".into(),
Expand Down Expand Up @@ -860,12 +863,14 @@ async fn resume_replays_image_tool_outputs_with_detail() {
let image_url = "data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEAAUAmJaACdLoB+AADsAD+8ut//NgVzXPv9//S4P0uD9Lg/9KQAAA=";
let function_call_id = "view-image-call";
let custom_call_id = "js-repl-call";
let thread_id = ThreadId::default();
let rollout = vec![
RolloutLine {
timestamp: "2024-01-01T00:00:00.000Z".to_string(),
item: RolloutItem::SessionMeta(SessionMetaLine {
meta: SessionMeta {
id: ThreadId::default(),
session_id: thread_id.into(),
id: thread_id,
parent_thread_id: None,
timestamp: "2024-01-01T00:00:00Z".to_string(),
cwd: ".".into(),
Expand Down
Loading
Loading