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
parent f50c02d7bc
commit fe05acad23
55 changed files with 1076 additions and 514 deletions
+2 -9
View File
@@ -430,6 +430,7 @@ impl TestCodexBuilder {
SessionSource::Exec,
Arc::clone(&environment_manager),
/*analytics_events_client*/ None,
thread_store_from_config(&config),
)
} else {
codex_core::test_support::thread_manager_with_models_provider_and_home(
@@ -449,7 +450,6 @@ impl TestCodexBuilder {
codex_core::test_support::resume_thread_from_rollout_with_user_shell_override(
thread_manager.as_ref(),
config.clone(),
thread_store_from_config(&config),
path,
auth_manager,
user_shell_override,
@@ -461,7 +461,6 @@ impl TestCodexBuilder {
let auth_manager = codex_core::test_support::auth_manager_from_auth(auth);
Box::pin(thread_manager.resume_thread_from_rollout(
config.clone(),
thread_store_from_config(&config),
path,
auth_manager,
/*parent_trace*/ None,
@@ -473,18 +472,12 @@ impl TestCodexBuilder {
codex_core::test_support::start_thread_with_user_shell_override(
thread_manager.as_ref(),
config.clone(),
thread_store_from_config(&config),
user_shell_override,
),
)
.await?
}
(None, None) => {
Box::pin(
thread_manager.start_thread(config.clone(), thread_store_from_config(&config)),
)
.await?
}
(None, None) => Box::pin(thread_manager.start_thread(config.clone())).await?,
};
Ok(TestCodex {
+2 -1
View File
@@ -1107,9 +1107,10 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
thread_store_from_config(&config),
);
let NewThread { thread: codex, .. } = thread_manager
.start_thread(config.clone(), thread_store_from_config(&config))
.start_thread(config.clone())
.await
.expect("create new conversation");
-1
View File
@@ -2556,7 +2556,6 @@ async fn code_mode_can_call_hidden_dynamic_tools() -> Result<()> {
.thread_manager
.start_thread_with_tools(
base_test.config.clone(),
codex_core::thread_store_from_config(&base_test.config),
vec![DynamicToolSpec {
namespace: Some("codex_app".to_string()),
name: "hidden_dynamic_tool".to_string(),
@@ -445,7 +445,6 @@ async fn remote_compact_filters_deferred_dynamic_tools() -> Result<()> {
.thread_manager
.start_thread_with_tools(
test.config.clone(),
codex_core::thread_store_from_config(&test.config),
dynamic_tools,
/*persist_extended_history*/ false,
)
@@ -149,6 +149,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() {
"compact+resume test expects base path {base_path:?} to exist",
);
shutdown_conversation(&base).await;
let resumed = resume_conversation(&manager, &config, base_path).await;
user_turn(&resumed, "AFTER_RESUME").await;
let resumed_path = fetch_conversation_path(&resumed);
@@ -304,6 +305,7 @@ async fn compact_resume_after_second_compaction_preserves_history() -> Result<()
"second compact test expects base path {base_path:?} to exist",
);
shutdown_conversation(&base).await;
let resumed = resume_conversation(&manager, &config, base_path).await;
user_turn(&resumed, "AFTER_RESUME").await;
let resumed_path = fetch_conversation_path(&resumed);
@@ -323,6 +325,7 @@ async fn compact_resume_after_second_compaction_preserves_history() -> Result<()
"second compact test expects forked path {forked_path:?} to exist",
);
shutdown_conversation(&forked).await;
let resumed_again = resume_conversation(&manager, &config, forked_path).await;
user_turn(&resumed_again, AFTER_SECOND_RESUME).await;
@@ -815,6 +818,13 @@ fn fetch_conversation_path(conversation: &Arc<CodexThread>) -> std::path::PathBu
conversation.rollout_path().expect("rollout path")
}
async fn shutdown_conversation(conversation: &Arc<CodexThread>) {
conversation
.shutdown_and_wait()
.await
.expect("shutdown conversation");
}
async fn resume_conversation(
manager: &ThreadManager,
config: &Config,
@@ -825,7 +835,6 @@ async fn resume_conversation(
);
Box::pin(manager.resume_thread_from_rollout(
config.clone(),
codex_core::thread_store_from_config(config),
path,
auth_manager,
/*parent_trace*/ None,
@@ -845,7 +854,6 @@ async fn fork_thread(
Box::pin(manager.fork_thread(
nth_user_message,
config.clone(),
codex_core::thread_store_from_config(config),
path,
/*persist_extended_history*/ false,
/*parent_trace*/ None,
-3
View File
@@ -100,7 +100,6 @@ async fn fork_thread_twice_drops_to_first_message() {
.fork_thread(
ForkSnapshot::TruncateBeforeNthUserMessage(1),
config_for_fork.clone(),
codex_core::thread_store_from_config(&config_for_fork),
base_path.clone(),
/*persist_extended_history*/ false,
/*parent_trace*/ None,
@@ -125,7 +124,6 @@ async fn fork_thread_twice_drops_to_first_message() {
.fork_thread(
ForkSnapshot::TruncateBeforeNthUserMessage(0),
config_for_fork.clone(),
codex_core::thread_store_from_config(&config_for_fork),
fork1_path.clone(),
/*persist_extended_history*/ false,
/*parent_trace*/ None,
@@ -194,7 +192,6 @@ async fn fork_thread_from_history_does_not_require_source_rollout_path() {
.fork_thread_from_history(
ForkSnapshot::Interrupted,
test.config.clone(),
codex_core::thread_store_from_config(&test.config),
InitialHistory::Resumed(ResumedHistory {
conversation_id: test.session_configured.session_id,
history: source_items.clone(),
@@ -496,7 +496,6 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
.fork_thread(
ForkSnapshot::Interrupted,
fork_config.clone(),
codex_core::thread_store_from_config(&fork_config),
rollout_path,
/*persist_extended_history*/ false,
/*parent_trace*/ None,
@@ -1671,7 +1671,6 @@ async fn conversation_startup_context_current_thread_selects_many_turns_by_budge
.thread_manager
.resume_thread_with_history(
test.config.clone(),
codex_core::thread_store_from_config(&test.config),
InitialHistory::Forked(history),
auth_manager_from_auth(CodexAuth::from_api_key("dummy")),
/*persist_extended_history*/ false,
@@ -106,7 +106,6 @@ async fn emits_warning_when_resumed_model_differs() {
} = thread_manager
.resume_thread_with_history(
config.clone(),
codex_core::thread_store_from_config(&config),
initial_history,
auth_manager,
/*persist_extended_history*/ false,
-1
View File
@@ -793,7 +793,6 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
.thread_manager
.start_thread_with_tools(
base_test.config.clone(),
codex_core::thread_store_from_config(&base_test.config),
vec![dynamic_tool],
/*persist_extended_history*/ false,
)
+2 -3
View File
@@ -246,10 +246,9 @@ async fn list_skills_skips_cwd_roots_when_environment_disabled() -> Result<()> {
)?,
)),
/*analytics_events_client*/ None,
thread_store_from_config(&config),
);
let new_thread = thread_manager
.start_thread(config.clone(), thread_store_from_config(&config))
.await?;
let new_thread = thread_manager.start_thread(config.clone()).await?;
let cwd = config.cwd.to_path_buf();
new_thread
@@ -44,7 +44,6 @@ async fn emits_warning_when_unstable_features_enabled_via_config() {
} = thread_manager
.resume_thread_with_history(
config.clone(),
codex_core::thread_store_from_config(&config),
InitialHistory::New,
auth_manager,
/*persist_extended_history*/ false,
@@ -92,7 +91,6 @@ async fn suppresses_warning_when_configured() {
} = thread_manager
.resume_thread_with_history(
config.clone(),
codex_core::thread_store_from_config(&config),
InitialHistory::New,
auth_manager,
/*persist_extended_history*/ false,
@@ -71,7 +71,6 @@ async fn window_id_advances_after_compact_persists_on_resume_and_resets_on_fork(
.fork_thread(
/*snapshot*/ 0usize,
resumed.config.clone(),
codex_core::thread_store_from_config(&resumed.config),
rollout_path,
/*persist_extended_history*/ false,
/*parent_trace*/ None,