mirror of
https://github.com/openai/codex.git
synced 2026-11-04 08:25:29 +00:00
Enforce single-writer ownership for all thread histories (#36389)
## Why Legacy thread histories did not use the cross-process writer ownership guard already applied to paginated histories. ## What changed - Acquire and retain a writer lock whenever a legacy or paginated thread is created or resumed. - Apply the same ownership checks when archiving or deleting either history mode. - Release ownership when initialization fails or the active thread shuts down, allowing another process to resume it. ## Testing - Cover competing create and resume operations for both history modes. - Cover archive and deletion conflicts for legacy and paginated descendants. GitOrigin-RevId: 1be79b01966ad2634ed79eb96c0aa0db8eafe2fa
This commit is contained in:
1 parent
1bef168976
commit
dc60dadce6
16 files changed
+240
-366
No files matched your search
@@ -549,6 +549,21 @@ impl TestCodexBuilder {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn restart(
|
||||
&mut self,
|
||||
server: &MockServer,
|
||||
previous: &TestCodex,
|
||||
) -> Result<TestCodex> {
|
||||
let rollout_path = previous
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.context("rollout path")?;
|
||||
previous.codex.shutdown_and_wait().await?;
|
||||
self.resume(server, Arc::clone(&previous.home), rollout_path)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn build_with_home_and_base_url(
|
||||
&mut self,
|
||||
base_url: String,
|
||||
|
||||
@@ -944,12 +944,6 @@ async fn resume_replays_collaboration_instructions() -> Result<()> {
|
||||
|
||||
let mut builder = test_codex();
|
||||
let initial = builder.build(&server).await?;
|
||||
let rollout_path = initial
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("rollout path");
|
||||
let home = initial.home.clone();
|
||||
|
||||
let collab_text = "resume instructions";
|
||||
core_test_support::submit_thread_settings(
|
||||
@@ -976,7 +970,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> {
|
||||
.await?;
|
||||
wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let resumed = builder.resume(&server, home, rollout_path).await?;
|
||||
let resumed = builder.restart(&server, &initial).await?;
|
||||
resumed
|
||||
.codex
|
||||
.submit(Op::UserInput {
|
||||
|
||||
@@ -2041,12 +2041,6 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() {
|
||||
let _ = config.features.disable(Feature::RemoteCompactionV2);
|
||||
});
|
||||
let initial = builder.build(&server).await.unwrap();
|
||||
let home = initial.home.clone();
|
||||
let rollout_path = initial
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("rollout path");
|
||||
|
||||
// A single over-limit completion should not auto-compact until the next user message.
|
||||
mount_sse_once(
|
||||
@@ -2069,10 +2063,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() {
|
||||
config.model_auto_compact_token_limit = Some(limit);
|
||||
let _ = config.features.disable(Feature::RemoteCompactionV2);
|
||||
});
|
||||
let resumed = resume_builder
|
||||
.resume(&server, home, rollout_path)
|
||||
.await
|
||||
.unwrap();
|
||||
let resumed = resume_builder.restart(&server, &initial).await.unwrap();
|
||||
|
||||
let follow_up_user = "AFTER_RESUME_USER";
|
||||
let sse_follow_up = sse(vec![
|
||||
|
||||
@@ -2005,12 +2005,6 @@ async fn resumed_thread_runs_resume_then_compact_session_start_hooks() -> Result
|
||||
trust_discovered_hooks(config);
|
||||
});
|
||||
let initial = builder.build(&server).await?;
|
||||
let home = initial.home.clone();
|
||||
let rollout_path = initial
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.context("rollout path")?;
|
||||
|
||||
initial.submit_turn("hello before resume").await?;
|
||||
assert_eq!(responses_mock.requests().len(), 1);
|
||||
@@ -2019,7 +2013,7 @@ async fn resumed_thread_runs_resume_then_compact_session_start_hooks() -> Result
|
||||
config.model_auto_compact_token_limit = Some(limit);
|
||||
trust_discovered_hooks(config);
|
||||
});
|
||||
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
|
||||
let resumed = resume_builder.restart(&server, &initial).await?;
|
||||
resumed.submit_turn("hello after resume").await?;
|
||||
|
||||
let requests = responses_mock.requests();
|
||||
@@ -2129,12 +2123,6 @@ async fn resumed_thread_keeps_stop_continuation_prompt_in_history() -> Result<()
|
||||
})
|
||||
.with_config(trust_discovered_hooks);
|
||||
let initial = initial_builder.build(&server).await?;
|
||||
let home = initial.home.clone();
|
||||
let rollout_path = initial
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("rollout path");
|
||||
|
||||
initial.submit_turn("tell me something").await?;
|
||||
|
||||
@@ -2151,7 +2139,7 @@ async fn resumed_thread_keeps_stop_continuation_prompt_in_history() -> Result<()
|
||||
.await;
|
||||
|
||||
let mut resume_builder = test_codex().with_config(trust_discovered_hooks);
|
||||
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
|
||||
let resumed = resume_builder.restart(&server, &initial).await?;
|
||||
|
||||
resumed.submit_turn("and now continue").await?;
|
||||
|
||||
|
||||
@@ -549,7 +549,6 @@ async fn deferred_tool_world_state_survives_resume_without_duplicate_updates() -
|
||||
|
||||
initial.codex.ensure_rollout_materialized().await;
|
||||
initial.codex.flush_rollout().await?;
|
||||
let home = initial.home.clone();
|
||||
let rollout_path = initial
|
||||
.session_configured
|
||||
.rollout_path
|
||||
@@ -573,11 +572,10 @@ async fn deferred_tool_world_state_survives_resume_without_duplicate_updates() -
|
||||
"mcp__codex_apps__calendar": "Plan events and manage your calendar."
|
||||
})
|
||||
);
|
||||
drop(initial);
|
||||
|
||||
let mut resume_builder = search_capable_apps_builder(apps_server.chatgpt_base_url)
|
||||
.with_config(enable_deferred_tool_world_state_without_agents);
|
||||
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
|
||||
let resumed = resume_builder.restart(&server, &initial).await?;
|
||||
drop(initial);
|
||||
wait_for_mcp_server(&resumed.codex, CODEX_APPS_MCP_SERVER_NAME).await?;
|
||||
resumed
|
||||
.submit_turn("inspect unchanged deferred tools after resume")
|
||||
|
||||
@@ -424,12 +424,6 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
|
||||
});
|
||||
let initial = initial_builder.build(&server).await?;
|
||||
let codex = Arc::clone(&initial.codex);
|
||||
let home = initial.home.clone();
|
||||
let rollout_path = initial
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("rollout path");
|
||||
|
||||
let initial_mock = mount_sse_once(
|
||||
&server,
|
||||
@@ -475,7 +469,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
|
||||
.expect("test config should allow feature update");
|
||||
config.personality = Some(Personality::Pragmatic);
|
||||
});
|
||||
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
|
||||
let resumed = resume_builder.restart(&server, &initial).await?;
|
||||
let resume_override_cwd = resumed.cwd_path().join(PRETURN_CONTEXT_DIFF_CWD);
|
||||
fs::create_dir_all(&resume_override_cwd)?;
|
||||
let resume_override_cwd = resume_override_cwd.abs();
|
||||
@@ -543,12 +537,6 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
|
||||
});
|
||||
let initial = initial_builder.build(&server).await?;
|
||||
let codex = Arc::clone(&initial.codex);
|
||||
let home = initial.home.clone();
|
||||
let rollout_path = initial
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("rollout path");
|
||||
|
||||
let initial_mock = mount_sse_once(
|
||||
&server,
|
||||
@@ -589,7 +577,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
|
||||
.with_config(|config| {
|
||||
config.model = Some("gpt-5.4".to_string());
|
||||
});
|
||||
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
|
||||
let resumed = resume_builder.restart(&server, &initial).await?;
|
||||
let resume_override_cwd = resumed.cwd_path().join(PRETURN_CONTEXT_DIFF_CWD);
|
||||
fs::create_dir_all(&resume_override_cwd)?;
|
||||
let resume_override_cwd = resume_override_cwd.abs();
|
||||
|
||||
@@ -225,21 +225,15 @@ async fn changing_configured_mode_hint_to_empty_emits_no_update() -> Result<()>
|
||||
.with_config(configure_custom_mode_hint)
|
||||
.build(&server)
|
||||
.await?;
|
||||
let home = initial.home.clone();
|
||||
let rollout_path = initial
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("rollout path");
|
||||
|
||||
submit_turn(&initial.codex, "before resume", /*effort*/ None).await?;
|
||||
drop(initial);
|
||||
|
||||
let mut resume_builder = test_codex().with_config(|config| {
|
||||
configure_multi_agent_v2(config);
|
||||
config.multi_agent_v2.multi_agent_mode_hint_text = Some(String::new());
|
||||
});
|
||||
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
|
||||
let resumed = resume_builder.restart(&server, &initial).await?;
|
||||
drop(initial);
|
||||
submit_turn(&resumed.codex, "after resume", /*effort*/ None).await?;
|
||||
|
||||
let requests = responses.requests();
|
||||
@@ -362,20 +356,14 @@ async fn leaving_ultra_after_cold_resume_emits_explicit_mode() -> Result<()> {
|
||||
.with_config(configure_ultra)
|
||||
.build(&server)
|
||||
.await?;
|
||||
let home = initial.home.clone();
|
||||
let rollout_path = initial
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("rollout path");
|
||||
|
||||
submit_turn(&initial.codex, "before resume", /*effort*/ None).await?;
|
||||
drop(initial);
|
||||
|
||||
let mut resume_builder = test_codex()
|
||||
.with_model_info_override("gpt-5.4", add_ultra_reasoning)
|
||||
.with_config(configure_ultra);
|
||||
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
|
||||
let resumed = resume_builder.restart(&server, &initial).await?;
|
||||
drop(initial);
|
||||
submit_turn(&resumed.codex, "after resume", Some(ReasoningEffort::High)).await?;
|
||||
|
||||
let requests = responses.requests();
|
||||
|
||||
@@ -245,12 +245,6 @@ async fn cold_root_resume_restores_agent_identity_and_role_on_followup() -> Resu
|
||||
});
|
||||
let initial = initial_builder.build_with_auto_env(&server).await?;
|
||||
let root_thread_id = initial.session_configured.thread_id;
|
||||
let home = initial.home.clone();
|
||||
let rollout_path = initial
|
||||
.codex
|
||||
.rollout_path()
|
||||
.expect("root rollout path")
|
||||
.to_path_buf();
|
||||
let mut op = vec![UserInput::Text {
|
||||
text: INITIAL_PROMPT.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -382,9 +376,10 @@ async fn cold_root_resume_restores_agent_identity_and_role_on_followup() -> Resu
|
||||
sibling_thread.flush_rollout().await?;
|
||||
worker_thread.flush_rollout().await?;
|
||||
initial.codex.flush_rollout().await?;
|
||||
sibling_thread.shutdown_and_wait().await?;
|
||||
worker_thread.shutdown_and_wait().await?;
|
||||
drop(sibling_thread);
|
||||
drop(worker_thread);
|
||||
drop(initial);
|
||||
|
||||
let followup_args = serde_json::to_string(&json!({
|
||||
"target": "worker",
|
||||
@@ -437,7 +432,8 @@ async fn cold_root_resume_restores_agent_identity_and_role_on_followup() -> Resu
|
||||
let mut resume_builder = test_codex().with_config(move |config| {
|
||||
configure_multi_agent_v2_with_role(config, &resumed_model_provider_base_url);
|
||||
});
|
||||
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
|
||||
let resumed = resume_builder.restart(&server, &initial).await?;
|
||||
drop(initial);
|
||||
assert_eq!(
|
||||
resumed.thread_manager.list_thread_ids().await,
|
||||
vec![root_thread_id]
|
||||
|
||||
@@ -631,12 +631,6 @@ async fn resume_replays_permissions_messages() -> Result<()> {
|
||||
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
});
|
||||
let initial = builder.build(&server).await?;
|
||||
let rollout_path = initial
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("rollout path");
|
||||
let home = initial.home.clone();
|
||||
|
||||
initial
|
||||
.codex
|
||||
@@ -677,7 +671,7 @@ async fn resume_replays_permissions_messages() -> Result<()> {
|
||||
.await?;
|
||||
wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let resumed = builder.resume(&server, home, rollout_path).await?;
|
||||
let resumed = builder.restart(&server, &initial).await?;
|
||||
resumed
|
||||
.codex
|
||||
.submit(Op::UserInput {
|
||||
@@ -736,7 +730,6 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("rollout path");
|
||||
let home = initial.home.clone();
|
||||
|
||||
initial
|
||||
.codex
|
||||
@@ -783,7 +776,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
|
||||
builder = builder.with_config(|config| {
|
||||
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted);
|
||||
});
|
||||
let resumed = builder.resume(&server, home, rollout_path.clone()).await?;
|
||||
let resumed = builder.restart(&server, &initial).await?;
|
||||
resumed
|
||||
.codex
|
||||
.submit(Op::UserInput {
|
||||
|
||||
@@ -13,49 +13,10 @@ use core_test_support::responses::mount_sse_sequence;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::TestCodexBuilder;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
use wiremock::MockServer;
|
||||
|
||||
async fn resume_until_initial_messages(
|
||||
builder: &mut TestCodexBuilder,
|
||||
server: &MockServer,
|
||||
home: Arc<TempDir>,
|
||||
rollout_path: PathBuf,
|
||||
predicate: impl Fn(&[EventMsg]) -> bool,
|
||||
) -> Result<TestCodex> {
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
|
||||
let poll_interval = Duration::from_millis(10);
|
||||
let mut last_initial_messages = "<missing initial messages>".to_string();
|
||||
|
||||
loop {
|
||||
let resumed = builder
|
||||
.resume(server, Arc::clone(&home), rollout_path.clone())
|
||||
.await?;
|
||||
if let Some(initial_messages) = resumed.session_configured.initial_messages.as_ref() {
|
||||
if predicate(initial_messages) {
|
||||
return Ok(resumed);
|
||||
}
|
||||
last_initial_messages = format!("{initial_messages:#?}");
|
||||
}
|
||||
|
||||
if tokio::time::Instant::now() >= deadline {
|
||||
panic!(
|
||||
"timed out waiting for rollout resume messages to stabilize: {last_initial_messages}"
|
||||
);
|
||||
}
|
||||
|
||||
drop(resumed);
|
||||
tokio::time::sleep(poll_interval).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn resume_includes_initial_messages_from_rollout_events() -> Result<()> {
|
||||
@@ -65,12 +26,6 @@ async fn resume_includes_initial_messages_from_rollout_events() -> Result<()> {
|
||||
let mut builder = test_codex();
|
||||
let initial = builder.build(&server).await?;
|
||||
let codex = Arc::clone(&initial.codex);
|
||||
let home = initial.home.clone();
|
||||
let rollout_path = initial
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("rollout path");
|
||||
|
||||
let initial_sse = sse(vec![
|
||||
ev_response_created("resp-initial"),
|
||||
@@ -98,26 +53,7 @@ async fn resume_includes_initial_messages_from_rollout_events() -> Result<()> {
|
||||
.await?;
|
||||
|
||||
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let resumed = resume_until_initial_messages(
|
||||
&mut builder,
|
||||
&server,
|
||||
home,
|
||||
rollout_path,
|
||||
|initial_messages| {
|
||||
matches!(
|
||||
initial_messages,
|
||||
[
|
||||
EventMsg::TurnStarted(_),
|
||||
EventMsg::UserMessage(_),
|
||||
EventMsg::AgentMessage(_),
|
||||
EventMsg::TokenCount(_),
|
||||
EventMsg::TurnComplete(_),
|
||||
]
|
||||
)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let resumed = builder.restart(&server, &initial).await?;
|
||||
let initial_messages = resumed
|
||||
.session_configured
|
||||
.initial_messages
|
||||
@@ -155,12 +91,6 @@ async fn resume_includes_initial_messages_from_reasoning_events() -> Result<()>
|
||||
});
|
||||
let initial = builder.build(&server).await?;
|
||||
let codex = Arc::clone(&initial.codex);
|
||||
let home = initial.home.clone();
|
||||
let rollout_path = initial
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("rollout path");
|
||||
|
||||
let initial_sse = sse(vec![
|
||||
ev_response_created("resp-initial"),
|
||||
@@ -184,28 +114,7 @@ async fn resume_includes_initial_messages_from_reasoning_events() -> Result<()>
|
||||
.await?;
|
||||
|
||||
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let resumed = resume_until_initial_messages(
|
||||
&mut builder,
|
||||
&server,
|
||||
home,
|
||||
rollout_path,
|
||||
|initial_messages| {
|
||||
matches!(
|
||||
initial_messages,
|
||||
[
|
||||
EventMsg::TurnStarted(_),
|
||||
EventMsg::UserMessage(_),
|
||||
EventMsg::AgentReasoning(_),
|
||||
EventMsg::AgentReasoningRawContent(_),
|
||||
EventMsg::AgentMessage(_),
|
||||
EventMsg::TokenCount(_),
|
||||
EventMsg::TurnComplete(_),
|
||||
]
|
||||
)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let resumed = builder.restart(&server, &initial).await?;
|
||||
let initial_messages = resumed
|
||||
.session_configured
|
||||
.initial_messages
|
||||
@@ -246,12 +155,6 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> {
|
||||
});
|
||||
let initial = builder.build(&server).await?;
|
||||
let codex = Arc::clone(&initial.codex);
|
||||
let home = initial.home.clone();
|
||||
let rollout_path = initial
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("rollout path");
|
||||
|
||||
let initial_sse = sse(vec![
|
||||
ev_response_created("resp-initial"),
|
||||
@@ -273,7 +176,6 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> {
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let initial_body = initial_mock.single_request().body_json();
|
||||
let initial_instructions = initial_body
|
||||
.get("instructions")
|
||||
@@ -301,7 +203,7 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> {
|
||||
let mut resume_builder = test_codex().with_config(|config| {
|
||||
config.model = Some("gpt-5.4".to_string());
|
||||
});
|
||||
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
|
||||
let resumed = resume_builder.restart(&server, &initial).await?;
|
||||
resumed
|
||||
.codex
|
||||
.submit(Op::UserInput {
|
||||
@@ -378,12 +280,6 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu
|
||||
});
|
||||
let initial = builder.build(&server).await?;
|
||||
let codex = Arc::clone(&initial.codex);
|
||||
let home = initial.home.clone();
|
||||
let rollout_path = initial
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("rollout path");
|
||||
|
||||
let initial_mock = mount_sse_once(
|
||||
&server,
|
||||
@@ -422,7 +318,7 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu
|
||||
let mut resume_builder = test_codex().with_config(|config| {
|
||||
config.model = Some("gpt-5.5".to_string());
|
||||
});
|
||||
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
|
||||
let resumed = resume_builder.restart(&server, &initial).await?;
|
||||
core_test_support::submit_thread_settings(
|
||||
&resumed.codex,
|
||||
codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
|
||||
@@ -181,6 +181,7 @@ async fn resume_restores_dynamic_tools_from_rollout_with_sqlite_enabled() -> Res
|
||||
})
|
||||
.await;
|
||||
|
||||
started.thread.shutdown_and_wait().await?;
|
||||
let mut resume_builder = test_codex().with_config(|config| {
|
||||
config
|
||||
.features
|
||||
|
||||
Reference in new issue
Block a user