Make thread store process-scoped (#19474)

- Build one app-server process ThreadStore from startup config and share
it with ThreadManager and CodexMessageProcessor.
- Remove per-thread/fork store reconstruction so effective thread config
cannot switch the persistence backend.
- Add params to ThreadStore create/resume for specifying thread
metadata, since otherwise the metadata from store creation would be used
(incorrectly).
This commit is contained in:
Tom
2026-04-30 21:24:59 -07:00
committed by GitHub
Unverified
parent f50c02d7bc
commit fe05acad23
55 changed files with 1076 additions and 514 deletions
+31 -43
View File
@@ -5,16 +5,12 @@ use crate::agent::role::DEFAULT_ROLE_NAME;
use crate::agent::role::resolve_role_config;
use crate::agent::status::is_final;
use crate::codex_thread::ThreadConfigSnapshot;
use crate::find_archived_thread_path_by_id_str;
use crate::find_thread_path_by_id_str;
use crate::rollout::RolloutRecorder;
use crate::session::emit_subagent_session_started;
use crate::session_prefix::format_subagent_context_line;
use crate::session_prefix::format_subagent_notification_message;
use crate::shell_snapshot::ShellSnapshot;
use crate::thread_manager::ResumeThreadFromRolloutOptions;
use crate::thread_manager::ResumeThreadWithHistoryOptions;
use crate::thread_manager::ThreadManagerState;
use crate::thread_manager::thread_store_from_config;
use crate::thread_rollout_truncation::truncate_rollout_to_last_n_fork_turns;
use codex_features::Feature;
use codex_protocol::AgentPath;
@@ -27,6 +23,7 @@ use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::InitialHistory;
use codex_protocol::protocol::InterAgentCommunication;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ResumedHistory;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
@@ -34,6 +31,7 @@ use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::user_input::UserInput;
use codex_rollout::state_db;
use codex_state::DirectionalThreadSpawnEdgeStatus;
use codex_thread_store::ReadThreadParams;
use serde::Serialize;
use std::collections::HashMap;
use std::collections::VecDeque;
@@ -235,7 +233,6 @@ impl AgentControl {
state
.spawn_new_thread_with_source(
config.clone(),
thread_store_from_config(&config),
self.clone(),
session_source,
/*persist_extended_history*/ false,
@@ -246,15 +243,7 @@ impl AgentControl {
)
.await?
}
(None, _) => {
state
.spawn_new_thread(
config.clone(),
thread_store_from_config(&config),
self.clone(),
)
.await?
}
(None, _) => state.spawn_new_thread(config.clone(), self.clone()).await?,
};
agent_metadata.agent_id = Some(new_thread.thread_id);
reservation.commit(agent_metadata.clone());
@@ -377,23 +366,21 @@ impl AgentControl {
parent_thread.codex.session.flush_rollout().await?;
}
let rollout_path = parent_thread
.as_ref()
.and_then(|parent_thread| parent_thread.rollout_path())
.or(find_thread_path_by_id_str(
config.codex_home.as_path(),
&parent_thread_id.to_string(),
)
.await?)
let parent_history = state
.read_stored_thread(ReadThreadParams {
thread_id: parent_thread_id,
include_archived: true,
include_history: true,
})
.await?
.history
.ok_or_else(|| {
CodexErr::Fatal(format!(
"parent thread rollout unavailable for fork: {parent_thread_id}"
"parent thread history unavailable for fork: {parent_thread_id}"
))
})?;
let mut forked_rollout_items = RolloutRecorder::get_rollout_history(&rollout_path)
.await?
.get_rollout_items();
let mut forked_rollout_items = parent_history.items;
if let SpawnAgentForkMode::LastNTurns(last_n_turns) = fork_mode {
forked_rollout_items =
truncate_rollout_to_last_n_fork_turns(&forked_rollout_items, *last_n_turns);
@@ -436,7 +423,6 @@ impl AgentControl {
state
.fork_thread_with_source(
config.clone(),
thread_store_from_config(&config),
InitialHistory::Forked(forked_rollout_items),
self.clone(),
session_source,
@@ -576,24 +562,26 @@ impl AgentControl {
let inherited_exec_policy = self
.inherited_exec_policy_for_source(&state, Some(&session_source), &config)
.await;
let rollout_path =
match find_thread_path_by_id_str(config.codex_home.as_path(), &thread_id.to_string())
.await?
{
Some(rollout_path) => rollout_path,
None => find_archived_thread_path_by_id_str(
config.codex_home.as_path(),
&thread_id.to_string(),
)
.await?
.ok_or_else(|| CodexErr::ThreadNotFound(thread_id))?,
};
let stored_thread = state
.read_stored_thread(ReadThreadParams {
thread_id,
include_archived: true,
include_history: true,
})
.await?;
let history = stored_thread
.history
.ok_or_else(|| CodexErr::ThreadNotFound(thread_id))?
.items;
let resumed_thread = state
.resume_thread_from_rollout_with_source(ResumeThreadFromRolloutOptions {
.resume_thread_with_history_with_source(ResumeThreadWithHistoryOptions {
config: config.clone(),
thread_store: thread_store_from_config(&config),
rollout_path,
initial_history: InitialHistory::Resumed(ResumedHistory {
conversation_id: thread_id,
history,
rollout_path: stored_thread.rollout_path,
}),
agent_control: self.clone(),
session_source,
inherited_shell_snapshot,
+6 -12
View File
@@ -7,7 +7,6 @@ use crate::config::Config;
use crate::config::ConfigBuilder;
use crate::context::ContextualUserFragment;
use crate::context::SubagentNotification;
use crate::thread_manager::thread_store_from_config;
use assert_matches::assert_matches;
use codex_features::Feature;
use codex_login::CodexAuth;
@@ -27,6 +26,7 @@ use codex_protocol::protocol::TurnCompleteEvent;
use codex_protocol::protocol::TurnStartedEvent;
use codex_thread_store::ArchiveThreadParams;
use codex_thread_store::LocalThreadStore;
use codex_thread_store::LocalThreadStoreConfig;
use codex_thread_store::ThreadStore;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
@@ -109,7 +109,7 @@ impl AgentControlHarness {
async fn start_thread(&self) -> (ThreadId, Arc<CodexThread>) {
let new_thread = self
.manager
.start_thread(self.config.clone(), thread_store_from_config(&self.config))
.start_thread(self.config.clone())
.await
.expect("start thread");
(new_thread.thread_id, new_thread.thread)
@@ -610,10 +610,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
Some("Child subagent guidance.".to_string());
let new_thread = harness
.manager
.start_thread(
parent_config.clone(),
thread_store_from_config(&parent_config),
)
.start_thread(parent_config.clone())
.await
.expect("start parent thread");
let parent_thread_id = new_thread.thread_id;
@@ -956,7 +953,7 @@ async fn spawn_agent_respects_max_threads_limit() {
let control = manager.agent_control();
let _ = manager
.start_thread(config.clone(), thread_store_from_config(&config))
.start_thread(config.clone())
.await
.expect("start thread");
@@ -1313,10 +1310,7 @@ async fn multi_agent_v2_completion_queues_message_for_direct_parent() {
let _ = tester_config.features.enable(Feature::MultiAgentV2);
let tester_thread_id = harness
.manager
.start_thread(
tester_config.clone(),
thread_store_from_config(&tester_config),
)
.start_thread(tester_config.clone())
.await
.expect("tester thread should start")
.thread_id;
@@ -1701,7 +1695,7 @@ async fn resume_agent_from_rollout_reads_archived_rollout_path() {
.shutdown_live_agent(child_thread_id)
.await
.expect("child shutdown should succeed");
let store = LocalThreadStore::new(codex_rollout::RolloutConfig::from_view(&harness.config));
let store = LocalThreadStore::new(LocalThreadStoreConfig::from_config(&harness.config));
store
.archive_thread(ArchiveThreadParams {
thread_id: child_thread_id,
+12
View File
@@ -25,6 +25,7 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionConfiguredEvent;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::Submission;
use codex_protocol::protocol::ThreadMemoryMode;
@@ -93,6 +94,7 @@ pub struct CodexThreadTurnContextOverrides {
pub struct CodexThread {
pub(crate) codex: Codex,
pub(crate) session_source: SessionSource,
session_configured: SessionConfiguredEvent,
rollout_path: Option<PathBuf>,
out_of_band_elicitation_count: Mutex<u64>,
_watch_registration: WatchRegistration,
@@ -103,6 +105,7 @@ pub struct CodexThread {
impl CodexThread {
pub(crate) fn new(
codex: Codex,
session_configured: SessionConfiguredEvent,
rollout_path: Option<PathBuf>,
session_source: SessionSource,
watch_registration: WatchRegistration,
@@ -110,6 +113,7 @@ impl CodexThread {
Self {
codex,
session_source,
session_configured,
rollout_path,
out_of_band_elicitation_count: Mutex::new(0),
_watch_registration: watch_registration,
@@ -377,6 +381,14 @@ impl CodexThread {
self.rollout_path.clone()
}
pub(crate) fn session_configured(&self) -> SessionConfiguredEvent {
self.session_configured.clone()
}
pub(crate) fn is_running(&self) -> bool {
!self.codex.tx_sub.is_closed()
}
pub async fn guardian_trunk_rollout_path(&self) -> Option<PathBuf> {
self.codex
.session
+3 -4
View File
@@ -3,6 +3,7 @@ use codex_config::config_toml::ConfigToml;
use codex_protocol::config_types::Personality;
use codex_thread_store::ListThreadsParams;
use codex_thread_store::LocalThreadStore;
use codex_thread_store::LocalThreadStoreConfig;
use codex_thread_store::ThreadSortKey;
use codex_thread_store::ThreadStore;
use std::io;
@@ -60,12 +61,10 @@ pub async fn maybe_migrate_personality(
}
async fn has_recorded_sessions(codex_home: &Path, default_provider: &str) -> io::Result<bool> {
let store = LocalThreadStore::new(codex_rollout::RolloutConfig {
let store = LocalThreadStore::new(LocalThreadStoreConfig {
codex_home: codex_home.to_path_buf(),
sqlite_home: codex_home.to_path_buf(),
cwd: codex_home.to_path_buf(),
model_provider_id: default_provider.to_string(),
generate_memories: false,
default_model_provider_id: default_provider.to_string(),
});
if has_threads(&store, /*archived*/ false).await? {
return Ok(true);
+2 -2
View File
@@ -41,9 +41,9 @@ pub async fn build_prompt_input(
SessionSource::Exec,
Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::new(local_runtime_paths)).await),
/*analytics_events_client*/ None,
thread_store_from_config(&config),
);
let thread_store = thread_store_from_config(&config);
let thread = thread_manager.start_thread(config, thread_store).await?;
let thread = thread_manager.start_thread(config).await?;
let output = build_prompt_input_from_session(thread.thread.codex.session.as_ref(), input).await;
let shutdown = thread.thread.shutdown_and_wait().await;
+2
View File
@@ -136,6 +136,7 @@ use codex_thread_store::LiveThreadInitGuard;
use codex_thread_store::LocalThreadStore;
use codex_thread_store::ResumeThreadParams;
use codex_thread_store::ThreadEventPersistenceMode;
use codex_thread_store::ThreadPersistenceMetadata;
use codex_thread_store::ThreadStore;
use codex_utils_output_truncation::TruncationPolicy;
use futures::future::BoxFuture;
@@ -347,6 +348,7 @@ use codex_protocol::protocol::SkillMetadata as ProtocolSkillMetadata;
use codex_protocol::protocol::SkillToolDependency as ProtocolSkillToolDependency;
use codex_protocol::protocol::StreamErrorEvent;
use codex_protocol::protocol::Submission;
use codex_protocol::protocol::ThreadMemoryMode;
use codex_protocol::protocol::TokenCountEvent;
use codex_protocol::protocol::TokenUsage;
use codex_protocol::protocol::TokenUsageInfo;
+18
View File
@@ -400,6 +400,15 @@ impl Session {
text: session_configuration.base_instructions.clone(),
},
dynamic_tools: session_configuration.dynamic_tools.clone(),
metadata: ThreadPersistenceMetadata {
cwd: Some(config.cwd.to_path_buf()),
model_provider: config.model_provider_id.clone(),
memory_mode: if config.memories.generate_memories {
ThreadMemoryMode::Enabled
} else {
ThreadMemoryMode::Disabled
},
},
event_persistence_mode,
},
)
@@ -413,6 +422,15 @@ impl Session {
rollout_path: resumed_history.rollout_path.clone(),
history: Some(resumed_history.history.clone()),
include_archived: true,
metadata: ThreadPersistenceMetadata {
cwd: Some(config.cwd.to_path_buf()),
model_provider: config.model_provider_id.clone(),
memory_mode: if config.memories.generate_memories {
ThreadMemoryMode::Enabled
} else {
ThreadMemoryMode::Disabled
},
},
event_persistence_mode,
},
)
+24 -7
View File
@@ -1679,9 +1679,6 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
.fork_thread(
usize::MAX,
fork_config.clone(),
std::sync::Arc::new(codex_thread_store::LocalThreadStore::new(
codex_rollout::RolloutConfig::from_view(&fork_config),
)),
rollout_path,
/*persist_extended_history*/ false,
/*parent_trace*/ None,
@@ -2720,6 +2717,7 @@ async fn wait_for_thread_rollback_failed(rx: &async_channel::Receiver<Event>) ->
}
async fn attach_thread_persistence(session: &mut Session) -> PathBuf {
let config = session.get_config().await;
let live_thread = LiveThread::create(
Arc::clone(&session.services.thread_store),
CreateThreadParams {
@@ -2728,6 +2726,15 @@ async fn attach_thread_persistence(session: &mut Session) -> PathBuf {
source: SessionSource::Exec,
base_instructions: BaseInstructions::default(),
dynamic_tools: Vec::new(),
metadata: ThreadPersistenceMetadata {
cwd: Some(config.cwd.to_path_buf()),
model_provider: config.model_provider_id.clone(),
memory_mode: if config.memories.generate_memories {
ThreadMemoryMode::Enabled
} else {
ThreadMemoryMode::Disabled
},
},
event_persistence_mode: ThreadEventPersistenceMode::Limited,
},
)
@@ -3392,7 +3399,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
Arc::new(codex_thread_store::LocalThreadStore::new(
codex_rollout::RolloutConfig::from_view(config.as_ref()),
codex_thread_store::LocalThreadStoreConfig::from_config(config.as_ref()),
)),
codex_rollout_trace::ThreadTraceContext::disabled(),
)
@@ -3539,7 +3546,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
state_db: None,
live_thread: None,
thread_store: Arc::new(codex_thread_store::LocalThreadStore::new(
codex_rollout::RolloutConfig::from_view(config.as_ref()),
codex_thread_store::LocalThreadStoreConfig::from_config(config.as_ref()),
)),
model_client: ModelClient::new(
Some(auth_manager.clone()),
@@ -3711,7 +3718,7 @@ async fn make_session_with_config_and_rx(
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
Arc::new(codex_thread_store::LocalThreadStore::new(
codex_rollout::RolloutConfig::from_view(config.as_ref()),
codex_thread_store::LocalThreadStoreConfig::from_config(config.as_ref()),
)),
codex_rollout_trace::ThreadTraceContext::disabled(),
)
@@ -4574,6 +4581,7 @@ async fn shutdown_complete_does_not_append_to_thread_store_after_shutdown() {
let (mut session, _turn_context) = make_session_and_context().await;
let store = Arc::new(codex_thread_store::InMemoryThreadStore::default());
let thread_store: Arc<dyn codex_thread_store::ThreadStore> = store.clone();
let config = session.get_config().await;
let live_thread = LiveThread::create(
Arc::clone(&thread_store),
CreateThreadParams {
@@ -4582,6 +4590,15 @@ async fn shutdown_complete_does_not_append_to_thread_store_after_shutdown() {
source: SessionSource::Exec,
base_instructions: BaseInstructions::default(),
dynamic_tools: Vec::new(),
metadata: ThreadPersistenceMetadata {
cwd: Some(config.cwd.to_path_buf()),
model_provider: config.model_provider_id.clone(),
memory_mode: if config.memories.generate_memories {
ThreadMemoryMode::Enabled
} else {
ThreadMemoryMode::Disabled
},
},
event_persistence_mode: ThreadEventPersistenceMode::Limited,
},
)
@@ -4968,7 +4985,7 @@ where
state_db: None,
live_thread: None,
thread_store: Arc::new(codex_thread_store::LocalThreadStore::new(
codex_rollout::RolloutConfig::from_view(config.as_ref()),
codex_thread_store::LocalThreadStoreConfig::from_config(config.as_ref()),
)),
model_client: ModelClient::new(
Some(Arc::clone(&auth_manager)),
@@ -729,7 +729,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager)));
let skills_watcher = Arc::new(SkillsWatcher::noop());
let thread_store = Arc::new(codex_thread_store::LocalThreadStore::new(
codex_rollout::RolloutConfig::from_view(&config),
codex_thread_store::LocalThreadStoreConfig::from_config(&config),
));
let CodexSpawnOk { codex, .. } = Codex::spawn(CodexSpawnArgs {
+1 -5
View File
@@ -20,7 +20,6 @@ use codex_models_manager::test_support::get_model_offline_for_tests;
use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelPreset;
use codex_thread_store::ThreadStore;
use once_cell::sync::Lazy;
use crate::ThreadManager;
@@ -77,18 +76,16 @@ pub fn thread_manager_with_models_provider_and_home(
pub async fn start_thread_with_user_shell_override(
thread_manager: &ThreadManager,
config: Config,
thread_store: Arc<dyn ThreadStore>,
user_shell_override: crate::shell::Shell,
) -> codex_protocol::error::Result<crate::NewThread> {
thread_manager
.start_thread_with_user_shell_override_for_tests(config, thread_store, user_shell_override)
.start_thread_with_user_shell_override_for_tests(config, user_shell_override)
.await
}
pub async fn resume_thread_from_rollout_with_user_shell_override(
thread_manager: &ThreadManager,
config: Config,
thread_store: Arc<dyn ThreadStore>,
rollout_path: PathBuf,
auth_manager: Arc<AuthManager>,
user_shell_override: crate::shell::Shell,
@@ -96,7 +93,6 @@ pub async fn resume_thread_from_rollout_with_user_shell_override(
thread_manager
.resume_thread_from_rollout_with_user_shell_override_for_tests(
config,
thread_store,
rollout_path,
auth_manager,
user_shell_override,
+98 -61
View File
@@ -28,6 +28,7 @@ use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_model_provider::create_model_provider;
use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::OPENAI_PROVIDER_ID;
use codex_models_manager::manager::RefreshStrategy;
use codex_models_manager::manager::SharedModelsManager;
use codex_protocol::ThreadId;
@@ -50,12 +51,15 @@ use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::protocol::TurnAbortedEvent;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::W3cTraceContext;
use codex_rollout::RolloutConfig;
use codex_state::DirectionalThreadSpawnEdgeStatus;
use codex_thread_store::InMemoryThreadStore;
use codex_thread_store::LocalThreadStore;
use codex_thread_store::LocalThreadStoreConfig;
use codex_thread_store::ReadThreadParams;
use codex_thread_store::RemoteThreadStore;
use codex_thread_store::StoredThread;
use codex_thread_store::ThreadStore;
use codex_thread_store::ThreadStoreError;
use codex_utils_absolute_path::AbsolutePathBuf;
use futures::StreamExt;
use futures::stream::FuturesUnordered;
@@ -211,7 +215,6 @@ pub struct ThreadManager {
pub struct StartThreadOptions {
pub config: Config,
pub thread_store: Arc<dyn ThreadStore>,
pub initial_history: InitialHistory,
pub session_source: Option<SessionSource>,
pub dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
@@ -221,10 +224,9 @@ pub struct StartThreadOptions {
pub environments: Vec<TurnEnvironmentSelection>,
}
pub(crate) struct ResumeThreadFromRolloutOptions {
pub(crate) struct ResumeThreadWithHistoryOptions {
pub(crate) config: Config,
pub(crate) thread_store: Arc<dyn ThreadStore>,
pub(crate) rollout_path: PathBuf,
pub(crate) initial_history: InitialHistory,
pub(crate) agent_control: AgentControl,
pub(crate) session_source: SessionSource,
pub(crate) inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
@@ -244,6 +246,7 @@ pub(crate) struct ThreadManagerState {
plugins_manager: Arc<PluginsManager>,
mcp_manager: Arc<McpManager>,
skills_watcher: Arc<SkillsWatcher>,
thread_store: Arc<dyn ThreadStore>,
session_source: SessionSource,
analytics_events_client: Option<AnalyticsEventsClient>,
// Captures submitted ops for testing purpose when test mode is enabled.
@@ -263,9 +266,9 @@ pub fn build_models_manager(
pub fn thread_store_from_config(config: &Config) -> Arc<dyn ThreadStore> {
match &config.experimental_thread_store {
ThreadStoreConfig::Local => {
Arc::new(LocalThreadStore::new(RolloutConfig::from_view(config)))
}
ThreadStoreConfig::Local => Arc::new(LocalThreadStore::new(
LocalThreadStoreConfig::from_config(config),
)),
ThreadStoreConfig::Remote { endpoint } => Arc::new(RemoteThreadStore::new(endpoint)),
ThreadStoreConfig::InMemory { id } => InMemoryThreadStore::for_id(id),
}
@@ -278,6 +281,7 @@ impl ThreadManager {
session_source: SessionSource,
environment_manager: Arc<EnvironmentManager>,
analytics_events_client: Option<AnalyticsEventsClient>,
thread_store: Arc<dyn ThreadStore>,
) -> Self {
let codex_home = config.codex_home.clone();
let restriction_product = session_source.restriction_product();
@@ -303,6 +307,7 @@ impl ThreadManager {
plugins_manager,
mcp_manager,
skills_watcher,
thread_store,
auth_manager,
session_source,
analytics_events_client,
@@ -363,6 +368,14 @@ impl ThreadManager {
restriction_product,
));
let skills_watcher = build_skills_watcher(Arc::clone(&skills_manager));
// This test constructor has no Config input. Tests that need a non-local
// process store should construct ThreadManager::new with an explicit store.
let thread_store: Arc<dyn ThreadStore> =
Arc::new(LocalThreadStore::new(LocalThreadStoreConfig {
codex_home: codex_home.clone(),
sqlite_home: codex_home.clone(),
default_model_provider_id: OPENAI_PROVIDER_ID.to_string(),
}));
Self {
state: Arc::new(ThreadManagerState {
threads: Arc::new(RwLock::new(HashMap::new())),
@@ -374,6 +387,7 @@ impl ThreadManager {
plugins_manager,
mcp_manager,
skills_watcher,
thread_store,
auth_manager,
session_source: SessionSource::Exec,
analytics_events_client: None,
@@ -517,16 +531,11 @@ impl ThreadManager {
Ok(subtree_thread_ids)
}
pub async fn start_thread(
&self,
config: Config,
thread_store: Arc<dyn ThreadStore>,
) -> CodexResult<NewThread> {
pub async fn start_thread(&self, config: Config) -> CodexResult<NewThread> {
// Box delegated thread-spawn futures so these convenience wrappers do
// not inline the full spawn path into every caller's async state.
Box::pin(self.start_thread_with_tools(
config,
thread_store,
Vec::new(),
/*persist_extended_history*/ false,
))
@@ -536,7 +545,6 @@ impl ThreadManager {
pub async fn start_thread_with_tools(
&self,
config: Config,
thread_store: Arc<dyn ThreadStore>,
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
persist_extended_history: bool,
) -> CodexResult<NewThread> {
@@ -546,7 +554,6 @@ impl ThreadManager {
);
Box::pin(self.start_thread_with_options(StartThreadOptions {
config,
thread_store,
initial_history: InitialHistory::New,
session_source: None,
dynamic_tools,
@@ -567,7 +574,6 @@ impl ThreadManager {
.unwrap_or_else(|| self.state.session_source.clone());
Box::pin(self.state.spawn_thread_with_source(
options.config,
options.thread_store,
options.initial_history,
Arc::clone(&self.state.auth_manager),
self.agent_control(),
@@ -587,7 +593,6 @@ impl ThreadManager {
pub async fn resume_thread_from_rollout(
&self,
config: Config,
thread_store: Arc<dyn ThreadStore>,
rollout_path: PathBuf,
auth_manager: Arc<AuthManager>,
parent_trace: Option<W3cTraceContext>,
@@ -595,7 +600,6 @@ impl ThreadManager {
let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?;
Box::pin(self.resume_thread_with_history(
config,
thread_store,
initial_history,
auth_manager,
/*persist_extended_history*/ false,
@@ -607,7 +611,6 @@ impl ThreadManager {
pub async fn resume_thread_with_history(
&self,
config: Config,
thread_store: Arc<dyn ThreadStore>,
initial_history: InitialHistory,
auth_manager: Arc<AuthManager>,
persist_extended_history: bool,
@@ -619,7 +622,6 @@ impl ThreadManager {
);
Box::pin(self.state.spawn_thread(
config,
thread_store,
initial_history,
auth_manager,
self.agent_control(),
@@ -636,7 +638,6 @@ impl ThreadManager {
pub(crate) async fn start_thread_with_user_shell_override_for_tests(
&self,
config: Config,
thread_store: Arc<dyn ThreadStore>,
user_shell_override: crate::shell::Shell,
) -> CodexResult<NewThread> {
let environments = default_thread_environment_selections(
@@ -645,7 +646,6 @@ impl ThreadManager {
);
Box::pin(self.state.spawn_thread(
config,
thread_store,
InitialHistory::New,
Arc::clone(&self.state.auth_manager),
self.agent_control(),
@@ -662,7 +662,6 @@ impl ThreadManager {
pub(crate) async fn resume_thread_from_rollout_with_user_shell_override_for_tests(
&self,
config: Config,
thread_store: Arc<dyn ThreadStore>,
rollout_path: PathBuf,
auth_manager: Arc<AuthManager>,
user_shell_override: crate::shell::Shell,
@@ -674,7 +673,6 @@ impl ThreadManager {
);
Box::pin(self.state.spawn_thread(
config,
thread_store,
initial_history,
auth_manager,
self.agent_control(),
@@ -754,7 +752,6 @@ impl ThreadManager {
&self,
snapshot: S,
config: Config,
thread_store: Arc<dyn ThreadStore>,
path: PathBuf,
persist_extended_history: bool,
parent_trace: Option<W3cTraceContext>,
@@ -767,7 +764,6 @@ impl ThreadManager {
self.fork_thread_from_history(
snapshot,
config,
thread_store,
history,
persist_extended_history,
parent_trace,
@@ -780,7 +776,6 @@ impl ThreadManager {
&self,
snapshot: S,
config: Config,
thread_store: Arc<dyn ThreadStore>,
history: InitialHistory,
persist_extended_history: bool,
parent_trace: Option<W3cTraceContext>,
@@ -791,7 +786,6 @@ impl ThreadManager {
self.fork_thread_with_initial_history(
snapshot.into(),
config,
thread_store,
history,
persist_extended_history,
parent_trace,
@@ -803,7 +797,6 @@ impl ThreadManager {
&self,
snapshot: ForkSnapshot,
config: Config,
thread_store: Arc<dyn ThreadStore>,
history: InitialHistory,
persist_extended_history: bool,
parent_trace: Option<W3cTraceContext>,
@@ -816,7 +809,6 @@ impl ThreadManager {
);
Box::pin(self.state.spawn_thread(
config,
thread_store,
history,
Arc::clone(&self.state.auth_manager),
self.agent_control(),
@@ -865,6 +857,31 @@ impl ThreadManagerState {
}
}
pub(crate) async fn read_stored_thread(
&self,
params: ReadThreadParams,
) -> CodexResult<StoredThread> {
let thread_id = params.thread_id;
self.thread_store
.read_thread(params)
.await
.map_err(|err| match err {
ThreadStoreError::ThreadNotFound { thread_id } => {
CodexErr::ThreadNotFound(thread_id)
}
ThreadStoreError::InvalidRequest { message } => {
if message.starts_with("no rollout found for thread id ") {
CodexErr::ThreadNotFound(thread_id)
} else {
CodexErr::Fatal(format!(
"failed to read stored thread {thread_id}: invalid thread-store request: {message}"
))
}
}
err => CodexErr::Fatal(format!("failed to read stored thread {thread_id}: {err}")),
})
}
/// Send an operation to a thread by ID.
pub(crate) async fn send_op(&self, thread_id: ThreadId, op: Op) -> CodexResult<String> {
let thread = self.get_thread(thread_id).await?;
@@ -896,12 +913,10 @@ impl ThreadManagerState {
pub(crate) async fn spawn_new_thread(
&self,
config: Config,
thread_store: Arc<dyn ThreadStore>,
agent_control: AgentControl,
) -> CodexResult<NewThread> {
Box::pin(self.spawn_new_thread_with_source(
config,
thread_store,
agent_control,
self.session_source.clone(),
/*persist_extended_history*/ false,
@@ -917,7 +932,6 @@ impl ThreadManagerState {
pub(crate) async fn spawn_new_thread_with_source(
&self,
config: Config,
thread_store: Arc<dyn ThreadStore>,
agent_control: AgentControl,
session_source: SessionSource,
persist_extended_history: bool,
@@ -931,7 +945,6 @@ impl ThreadManagerState {
});
Box::pin(self.spawn_thread_with_source(
config,
thread_store,
InitialHistory::New,
Arc::clone(&self.auth_manager),
agent_control,
@@ -948,25 +961,22 @@ impl ThreadManagerState {
.await
}
pub(crate) async fn resume_thread_from_rollout_with_source(
pub(crate) async fn resume_thread_with_history_with_source(
&self,
options: ResumeThreadFromRolloutOptions,
options: ResumeThreadWithHistoryOptions,
) -> CodexResult<NewThread> {
let ResumeThreadFromRolloutOptions {
let ResumeThreadWithHistoryOptions {
config,
thread_store,
rollout_path,
initial_history,
agent_control,
session_source,
inherited_shell_snapshot,
inherited_exec_policy,
} = options;
let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?;
let environments =
default_thread_environment_selections(self.environment_manager.as_ref(), &config.cwd);
Box::pin(self.spawn_thread_with_source(
config,
thread_store,
initial_history,
Arc::clone(&self.auth_manager),
agent_control,
@@ -987,7 +997,6 @@ impl ThreadManagerState {
pub(crate) async fn fork_thread_with_source(
&self,
config: Config,
thread_store: Arc<dyn ThreadStore>,
initial_history: InitialHistory,
agent_control: AgentControl,
session_source: SessionSource,
@@ -1001,7 +1010,6 @@ impl ThreadManagerState {
});
Box::pin(self.spawn_thread_with_source(
config,
thread_store,
initial_history,
Arc::clone(&self.auth_manager),
agent_control,
@@ -1023,7 +1031,6 @@ impl ThreadManagerState {
pub(crate) async fn spawn_thread(
&self,
config: Config,
thread_store: Arc<dyn ThreadStore>,
initial_history: InitialHistory,
auth_manager: Arc<AuthManager>,
agent_control: AgentControl,
@@ -1036,7 +1043,6 @@ impl ThreadManagerState {
) -> CodexResult<NewThread> {
Box::pin(self.spawn_thread_with_source(
config,
thread_store,
initial_history,
auth_manager,
agent_control,
@@ -1057,7 +1063,6 @@ impl ThreadManagerState {
pub(crate) async fn spawn_thread_with_source(
&self,
config: Config,
thread_store: Arc<dyn ThreadStore>,
initial_history: InitialHistory,
auth_manager: Arc<AuthManager>,
agent_control: AgentControl,
@@ -1072,6 +1077,27 @@ impl ThreadManagerState {
user_shell_override: Option<crate::shell::Shell>,
) -> CodexResult<NewThread> {
let is_resumed_thread = matches!(&initial_history, InitialHistory::Resumed(_));
if let InitialHistory::Resumed(resumed) = &initial_history {
let mut threads = self.threads.write().await;
if let Some(thread) = threads.get(&resumed.conversation_id).cloned() {
if thread.is_running() {
if let Some(requested_rollout_path) = resumed.rollout_path.as_deref()
&& thread.rollout_path().as_deref() != Some(requested_rollout_path)
{
return Err(CodexErr::InvalidRequest(format!(
"thread {} is already running with a different rollout path",
resumed.conversation_id
)));
}
return Ok(NewThread {
thread_id: resumed.conversation_id,
session_configured: thread.session_configured(),
thread,
});
}
threads.remove(&resumed.conversation_id);
}
}
let environment =
selected_primary_environment(self.environment_manager.as_ref(), &environments)?;
let watch_registration = match environment.as_ref() {
@@ -1115,7 +1141,7 @@ impl ThreadManagerState {
parent_trace,
environments,
analytics_events_client: self.analytics_events_client.clone(),
thread_store,
thread_store: Arc::clone(&self.thread_store),
})
.await?;
let new_thread = self
@@ -1147,20 +1173,31 @@ impl ThreadManagerState {
}
};
let thread = Arc::new(CodexThread::new(
codex,
session_configured.rollout_path.clone(),
session_source,
watch_registration,
));
let mut threads = self.threads.write().await;
threads.insert(thread_id, thread.clone());
{
let mut threads = self.threads.write().await;
if let std::collections::hash_map::Entry::Vacant(e) = threads.entry(thread_id) {
let thread = Arc::new(CodexThread::new(
codex,
session_configured.clone(),
session_configured.rollout_path.clone(),
session_source,
watch_registration,
));
e.insert(thread.clone());
return Ok(NewThread {
thread_id,
thread,
session_configured,
});
}
}
Ok(NewThread {
thread_id,
thread,
session_configured,
})
if let Err(err) = codex.shutdown_and_wait().await {
warn!("failed to shut down duplicate thread {thread_id}: {err}");
}
Err(CodexErr::InvalidRequest(format!(
"thread {thread_id} is already running"
)))
}
pub(crate) fn notify_thread_created(&self, thread_id: ThreadId) {
+121 -21
View File
@@ -161,8 +161,7 @@ fn fork_thread_accepts_legacy_usize_snapshot_argument() {
) {
let _future = manager.fork_thread(
usize::MAX,
config.clone(),
thread_store_from_config(&config),
config,
path,
/*persist_extended_history*/ false,
/*parent_trace*/ None,
@@ -263,12 +262,12 @@ async fn shutdown_all_threads_bounded_submits_shutdown_to_every_thread() {
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
);
let thread_1 = manager
.start_thread(config.clone(), thread_store_from_config(&config))
.start_thread(config.clone())
.await
.expect("start first thread")
.thread_id;
let thread_2 = manager
.start_thread(config.clone(), thread_store_from_config(&config))
.start_thread(config.clone())
.await
.expect("start second thread")
.thread_id;
@@ -314,7 +313,6 @@ async fn start_thread_accepts_explicit_environment_when_default_environment_is_d
let thread = manager
.start_thread_with_options(StartThreadOptions {
thread_store: thread_store_from_config(&config),
config: config.clone(),
initial_history: InitialHistory::New,
session_source: None,
@@ -347,10 +345,8 @@ async fn start_thread_keeps_internal_threads_hidden_from_normal_lookups() {
config.codex_home.to_path_buf(),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
);
let thread_store = thread_store_from_config(&config);
let thread = manager
.start_thread_with_options(StartThreadOptions {
thread_store,
config,
initial_history: InitialHistory::New,
session_source: Some(SessionSource::Internal(
@@ -393,6 +389,7 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
thread_store_from_config(&config),
);
let selected_cwd =
AbsolutePathBuf::try_from(config.cwd.as_path().join("selected")).expect("absolute path");
@@ -401,11 +398,8 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
cwd: selected_cwd.clone(),
}];
let default_cwd = config.cwd.clone();
let thread_store = thread_store_from_config(&config);
let source = manager
.start_thread_with_options(StartThreadOptions {
thread_store: Arc::clone(&thread_store),
config: config.clone(),
initial_history: InitialHistory::New,
session_source: None,
@@ -437,7 +431,6 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
let resumed = manager
.resume_thread_from_rollout(
config.clone(),
Arc::clone(&thread_store),
rollout_path.clone(),
auth_manager,
/*parent_trace*/ None,
@@ -459,7 +452,6 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
.fork_thread(
ForkSnapshot::Interrupted,
config,
thread_store,
rollout_path,
/*persist_extended_history*/ false,
/*parent_trace*/ None,
@@ -478,6 +470,117 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
assert_ne!(forked_turn.environments[0].cwd, selected_cwd);
}
#[tokio::test]
async fn resume_active_thread_from_rollout_returns_running_thread() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
thread_store_from_config(&config),
);
let source = manager
.start_thread(config.clone())
.await
.expect("start source thread");
source.thread.ensure_rollout_materialized().await;
source
.thread
.flush_rollout()
.await
.expect("flush source rollout");
let rollout_path = source
.thread
.rollout_path()
.expect("source rollout path should exist");
let resumed = manager
.resume_thread_from_rollout(
config,
rollout_path,
auth_manager,
/*parent_trace*/ None,
)
.await
.expect("resume active source thread");
assert_eq!(resumed.thread_id, source.thread_id);
assert!(Arc::ptr_eq(&resumed.thread, &source.thread));
source
.thread
.shutdown_and_wait()
.await
.expect("shutdown source thread");
}
#[tokio::test]
async fn resume_stopped_thread_from_rollout_spawns_new_thread() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
thread_store_from_config(&config),
);
let source = manager
.start_thread(config.clone())
.await
.expect("start source thread");
source.thread.ensure_rollout_materialized().await;
source
.thread
.flush_rollout()
.await
.expect("flush source rollout");
let rollout_path = source
.thread
.rollout_path()
.expect("source rollout path should exist");
source
.thread
.shutdown_and_wait()
.await
.expect("shutdown source thread");
let resumed = manager
.resume_thread_from_rollout(
config,
rollout_path,
auth_manager,
/*parent_trace*/ None,
)
.await
.expect("resume stopped source thread");
assert_eq!(resumed.thread_id, source.thread_id);
assert!(!Arc::ptr_eq(&resumed.thread, &source.thread));
resumed
.thread
.shutdown_and_wait()
.await
.expect("shutdown resumed thread");
}
#[tokio::test]
async fn new_uses_active_provider_for_model_refresh() {
let server = MockServer::start().await;
@@ -499,6 +602,7 @@ async fn new_uses_active_provider_for_model_refresh() {
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
thread_store_from_config(&config),
);
let _ = manager.list_models(RefreshStrategy::Online).await;
@@ -709,12 +813,12 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
thread_store_from_config(&config),
);
let source = manager
.resume_thread_with_history(
config.clone(),
thread_store_from_config(&config),
InitialHistory::Forked(vec![
RolloutItem::ResponseItem(user_msg("hello")),
RolloutItem::ResponseItem(assistant_msg("partial")),
@@ -741,7 +845,6 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
.fork_thread(
ForkSnapshot::Interrupted,
config.clone(),
thread_store_from_config(&config),
source_path,
/*persist_extended_history*/ false,
/*parent_trace*/ None,
@@ -812,12 +915,12 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
thread_store_from_config(&config),
);
let source = manager
.resume_thread_with_history(
config.clone(),
thread_store_from_config(&config),
InitialHistory::Forked(vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-explicit".to_string(),
@@ -855,7 +958,6 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
.fork_thread(
ForkSnapshot::Interrupted,
config.clone(),
thread_store_from_config(&config),
source_path,
/*persist_extended_history*/ false,
/*parent_trace*/ None,
@@ -904,12 +1006,12 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
thread_store_from_config(&config),
);
let source = manager
.resume_thread_with_history(
config.clone(),
thread_store_from_config(&config),
InitialHistory::Forked(vec![
RolloutItem::ResponseItem(user_msg("hello")),
RolloutItem::ResponseItem(assistant_msg("partial")),
@@ -934,7 +1036,6 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
.fork_thread(
ForkSnapshot::Interrupted,
config.clone(),
thread_store_from_config(&config),
source_path,
/*persist_extended_history*/ false,
/*parent_trace*/ None,
@@ -975,7 +1076,6 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
.fork_thread(
ForkSnapshot::Interrupted,
config.clone(),
thread_store_from_config(&config),
forked_path,
/*persist_extended_history*/ false,
/*parent_trace*/ None,
@@ -1042,12 +1142,12 @@ async fn resumed_thread_activates_paused_goal_and_continues_on_request() -> anyh
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
thread_store_from_config(&config),
);
let source = manager
.resume_thread_with_history(
config.clone(),
thread_store_from_config(&config),
InitialHistory::Forked(vec![RolloutItem::ResponseItem(user_msg("keep working"))]),
auth_manager.clone(),
/*persist_extended_history*/ false,
@@ -1072,12 +1172,12 @@ async fn resumed_thread_activates_paused_goal_and_continues_on_request() -> anyh
/*token_budget*/ None,
)
.await?;
source.thread.shutdown_and_wait().await?;
manager.remove_thread(&source.thread_id).await;
let resumed = manager
.resume_thread_from_rollout(
config.clone(),
thread_store_from_config(&config),
source_path,
auth_manager,
/*parent_trace*/ None,
@@ -5,7 +5,6 @@ use crate::config::DEFAULT_AGENT_MAX_DEPTH;
use crate::function_tool::FunctionCallError;
use crate::session::tests::make_session_and_context;
use crate::session_prefix::format_subagent_notification_message;
use crate::thread_manager::thread_store_from_config;
use crate::tools::context::ToolOutput;
use crate::tools::handlers::multi_agents_v2::CloseAgentHandler as CloseAgentHandlerV2;
use crate::tools::handlers::multi_agents_v2::FollowupTaskHandler as FollowupTaskHandlerV2;
@@ -297,10 +296,7 @@ async fn spawn_agent_fork_context_rejects_agent_type_override() {
let role_name = install_role_with_model_override(&mut turn).await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -332,10 +328,7 @@ async fn spawn_agent_fork_context_rejects_child_model_overrides() {
let (mut session, turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -370,10 +363,7 @@ async fn multi_agent_v2_spawn_fork_turns_all_rejects_agent_type_override() {
let role_name = install_role_with_model_override(&mut turn).await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -416,10 +406,7 @@ async fn multi_agent_v2_spawn_defaults_to_full_fork_and_rejects_child_model_over
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -460,10 +447,7 @@ async fn multi_agent_v2_spawn_partial_fork_turns_allows_agent_type_override() {
let role_name = install_role_with_model_override(&mut turn).await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -546,10 +530,7 @@ async fn multi_agent_v2_spawn_requires_task_name() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -583,10 +564,7 @@ async fn multi_agent_v2_spawn_rejects_legacy_items_field() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -646,10 +624,7 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -746,10 +721,7 @@ async fn multi_agent_v2_spawn_rejects_legacy_fork_context() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -788,10 +760,7 @@ async fn multi_agent_v2_spawn_rejects_invalid_fork_turns_string() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -830,10 +799,7 @@ async fn multi_agent_v2_spawn_rejects_zero_fork_turns() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -872,10 +838,7 @@ async fn multi_agent_v2_send_message_accepts_root_target_from_child() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -951,10 +914,7 @@ async fn multi_agent_v2_followup_task_rejects_root_target_from_child() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -1035,10 +995,7 @@ async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_messa
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -1132,10 +1089,7 @@ async fn multi_agent_v2_list_agents_filters_by_relative_path_prefix() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -1222,10 +1176,7 @@ async fn multi_agent_v2_list_agents_omits_closed_agents() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -1289,10 +1240,7 @@ async fn multi_agent_v2_send_message_rejects_legacy_items_field() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -1348,10 +1296,7 @@ async fn multi_agent_v2_send_message_rejects_interrupt_parameter() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -1424,10 +1369,7 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn()
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -1562,10 +1504,7 @@ async fn multi_agent_v2_followup_task_rejects_legacy_items_field() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -1618,10 +1557,7 @@ async fn multi_agent_v2_interrupted_turn_does_not_notify_parent() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -1698,10 +1634,7 @@ async fn multi_agent_v2_spawn_omits_agent_id_when_named() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -1740,10 +1673,7 @@ async fn multi_agent_v2_spawn_surfaces_task_name_validation_errors() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -1957,7 +1887,7 @@ async fn multi_agent_v2_spawn_agent_ignores_configured_max_depth() {
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
let root = manager
.start_thread(config.clone(), thread_store_from_config(&config))
.start_thread(config.clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -2082,7 +2012,7 @@ async fn send_input_interrupts_before_prompt() {
session.services.agent_control = manager.agent_control();
let config = turn.config.as_ref().clone();
let thread = manager
.start_thread(config.clone(), thread_store_from_config(&config))
.start_thread(config.clone())
.await
.expect("start thread");
let agent_id = thread.thread_id;
@@ -2124,7 +2054,7 @@ async fn send_input_accepts_structured_items() {
session.services.agent_control = manager.agent_control();
let config = turn.config.as_ref().clone();
let thread = manager
.start_thread(config.clone(), thread_store_from_config(&config))
.start_thread(config.clone())
.await
.expect("start thread");
let agent_id = thread.thread_id;
@@ -2219,7 +2149,7 @@ async fn resume_agent_noops_for_active_agent() {
session.services.agent_control = manager.agent_control();
let config = turn.config.as_ref().clone();
let thread = manager
.start_thread(config.clone(), thread_store_from_config(&config))
.start_thread(config.clone())
.await
.expect("start thread");
let agent_id = thread.thread_id;
@@ -2260,7 +2190,6 @@ async fn resume_agent_restores_closed_agent_and_accepts_send_input() {
let thread = manager
.resume_thread_with_history(
config.clone(),
thread_store_from_config(&config),
InitialHistory::Forked(vec![RolloutItem::ResponseItem(ResponseItem::Message {
id: None,
role: "user".to_string(),
@@ -2425,10 +2354,7 @@ async fn multi_agent_v2_wait_agent_accepts_timeout_only_argument() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -2605,7 +2531,7 @@ async fn wait_agent_times_out_when_status_is_not_final() {
session.services.agent_control = manager.agent_control();
let config = turn.config.as_ref().clone();
let thread = manager
.start_thread(config.clone(), thread_store_from_config(&config))
.start_thread(config.clone())
.await
.expect("start thread");
let agent_id = thread.thread_id;
@@ -2648,7 +2574,7 @@ async fn wait_agent_clamps_short_timeouts_to_minimum() {
session.services.agent_control = manager.agent_control();
let config = turn.config.as_ref().clone();
let thread = manager
.start_thread(config.clone(), thread_store_from_config(&config))
.start_thread(config.clone())
.await
.expect("start thread");
let agent_id = thread.thread_id;
@@ -2686,7 +2612,7 @@ async fn wait_agent_returns_final_status_without_timeout() {
session.services.agent_control = manager.agent_control();
let config = turn.config.as_ref().clone();
let thread = manager
.start_thread(config.clone(), thread_store_from_config(&config))
.start_thread(config.clone())
.await
.expect("start thread");
let agent_id = thread.thread_id;
@@ -2736,10 +2662,7 @@ async fn multi_agent_v2_wait_agent_returns_summary_for_mailbox_activity() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -2830,10 +2753,7 @@ async fn multi_agent_v2_wait_agent_returns_for_already_queued_mail() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -2911,10 +2831,7 @@ async fn multi_agent_v2_wait_agent_wakes_on_any_mailbox_notification() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -3002,10 +2919,7 @@ async fn multi_agent_v2_wait_agent_does_not_return_completed_content() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -3091,10 +3005,7 @@ async fn multi_agent_v2_close_agent_accepts_task_name_target() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -3153,10 +3064,7 @@ async fn multi_agent_v2_close_agent_rejects_root_target_and_id() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let root = manager
.start_thread(
(*turn.config).clone(),
thread_store_from_config(turn.config.as_ref()),
)
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
@@ -3206,7 +3114,7 @@ async fn close_agent_submits_shutdown_and_returns_previous_status() {
session.services.agent_control = manager.agent_control();
let config = turn.config.as_ref().clone();
let thread = manager
.start_thread(config.clone(), thread_store_from_config(&config))
.start_thread(config.clone())
.await
.expect("start thread");
let agent_id = thread.thread_id;
@@ -3250,7 +3158,7 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
.expect("test config should allow sqlite");
let parent = manager
.start_thread(config.clone(), thread_store_from_config(&config))
.start_thread(config.clone())
.await
.expect("parent thread should start");
let parent_thread_id = parent.thread_id;
@@ -3381,7 +3289,7 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
);
let operator = manager
.start_thread(config.clone(), thread_store_from_config(&config))
.start_thread(config.clone())
.await
.expect("operator thread should start");
let operator_session = operator.thread.codex.session.clone();