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
@@ -2687,12 +2687,7 @@ mod tests {
thread_id: conversation_id,
thread: conversation,
..
} = thread_manager
.start_thread(
config.clone(),
codex_core::thread_store_from_config(&config),
)
.await?;
} = thread_manager.start_thread(config.clone()).await?;
let thread_state = new_thread_state();
let thread_watch_manager = ThreadWatchManager::new();
let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY);
@@ -259,7 +259,6 @@ use codex_core::ThreadManager;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::NetworkProxyAuditMetadata;
use codex_core::config::ThreadStoreConfig;
use codex_core::config::edit::ConfigEdit;
use codex_core::config::edit::ConfigEditsBuilder;
use codex_core::exec::ExecCapturePolicy;
@@ -378,12 +377,10 @@ use codex_state::ThreadMetadata;
use codex_state::ThreadMetadataBuilder;
use codex_state::log_db::LogDbLayer;
use codex_thread_store::ArchiveThreadParams as StoreArchiveThreadParams;
use codex_thread_store::InMemoryThreadStore;
use codex_thread_store::ListThreadsParams as StoreListThreadsParams;
use codex_thread_store::LocalThreadStore;
use codex_thread_store::ReadThreadByRolloutPathParams as StoreReadThreadByRolloutPathParams;
use codex_thread_store::ReadThreadParams as StoreReadThreadParams;
use codex_thread_store::RemoteThreadStore;
use codex_thread_store::SortDirection as StoreSortDirection;
use codex_thread_store::StoredThread;
use codex_thread_store::ThreadMetadataPatch as StoreThreadMetadataPatch;
@@ -691,18 +688,11 @@ pub(crate) struct CodexMessageProcessorArgs {
/// go through `config_manager`.
pub(crate) config: Arc<Config>,
pub(crate) config_manager: ConfigManager,
pub(crate) thread_store: Arc<dyn ThreadStore>,
pub(crate) feedback: CodexFeedback,
pub(crate) log_db: Option<LogDbLayer>,
}
fn thread_store_from_config(config: &Config) -> Arc<dyn ThreadStore> {
match &config.experimental_thread_store {
ThreadStoreConfig::Local => Arc::new(configured_local_thread_store(config)),
ThreadStoreConfig::Remote { endpoint } => Arc::new(RemoteThreadStore::new(endpoint)),
ThreadStoreConfig::InMemory { id } => InMemoryThreadStore::for_id(id),
}
}
fn environment_selection_error_message(err: CodexErr) -> String {
match err {
CodexErr::InvalidRequest(message) => message,
@@ -710,10 +700,6 @@ fn environment_selection_error_message(err: CodexErr) -> String {
}
}
fn configured_local_thread_store(config: &Config) -> LocalThreadStore {
LocalThreadStore::new(codex_rollout::RolloutConfig::from_view(config))
}
impl CodexMessageProcessor {
async fn instruction_sources_from_config(config: &Config) -> Vec<AbsolutePathBuf> {
codex_core::AgentsMdManager::new(config)
@@ -830,6 +816,7 @@ impl CodexMessageProcessor {
arg0_paths,
config,
config_manager,
thread_store,
feedback,
log_db,
} = args;
@@ -839,7 +826,7 @@ impl CodexMessageProcessor {
outgoing: outgoing.clone(),
analytics_events_client,
arg0_paths,
thread_store: thread_store_from_config(&config),
thread_store,
config,
config_manager,
active_login: Arc::new(Mutex::new(None)),
@@ -2586,7 +2573,6 @@ impl CodexMessageProcessor {
let imported_thread = self
.thread_manager
.start_thread_with_options(StartThreadOptions {
thread_store: thread_store_from_config(&config),
config,
initial_history: InitialHistory::Forked(rollout_items),
session_source: None,
@@ -2784,7 +2770,6 @@ impl CodexMessageProcessor {
} = listener_task_context
.thread_manager
.start_thread_with_options(StartThreadOptions {
thread_store: thread_store_from_config(&config),
config,
initial_history: match session_start_source
.unwrap_or(codex_app_server_protocol::ThreadStartSource::Startup)
@@ -4348,7 +4333,6 @@ impl CodexMessageProcessor {
.thread_manager
.resume_thread_with_history(
config.clone(),
thread_store_from_config(&config),
thread_history,
self.auth_manager.clone(),
persist_extended_history,
@@ -4503,27 +4487,20 @@ impl CodexMessageProcessor {
request_id: &ConnectionRequestId,
params: &ThreadResumeParams,
) -> Result<bool, JSONRPCErrorError> {
if let Ok(existing_thread_id) = ThreadId::from_string(&params.thread_id)
&& let Ok(existing_thread) = self.thread_manager.get_thread(existing_thread_id).await
{
if params.history.is_some() {
let running_thread = if params.history.is_some() {
if let Ok(existing_thread_id) = ThreadId::from_string(&params.thread_id)
&& self
.thread_manager
.get_thread(existing_thread_id)
.await
.is_ok()
{
return Err(invalid_request(format!(
"cannot resume thread {existing_thread_id} with history while it is already running"
)));
}
if let (Some(requested_path), Some(active_path)) = (
params.path.as_ref(),
existing_thread.rollout_path().as_ref(),
) && requested_path != active_path
{
return Err(invalid_request(format!(
"cannot resume running thread {existing_thread_id} with mismatched path: requested `{}`, active `{}`",
requested_path.display(),
active_path.display()
)));
}
None
} else if params.path.is_some() {
let source_thread = self
.read_stored_thread_for_resume(
&params.thread_id,
@@ -4531,12 +4508,45 @@ impl CodexMessageProcessor {
/*include_history*/ true,
)
.await?;
let existing_thread_id = source_thread.thread_id;
if let Ok(existing_thread) = self.thread_manager.get_thread(existing_thread_id).await {
if let (Some(requested_path), Some(active_path)) = (
params.path.as_ref(),
existing_thread.rollout_path().as_ref(),
) && requested_path != active_path
{
return Err(invalid_request(format!(
"cannot resume running thread {existing_thread_id} with stale path: requested `{}`, active `{}`",
requested_path.display(),
active_path.display()
)));
}
Some((existing_thread_id, existing_thread, source_thread))
} else {
None
}
} else if let Ok(existing_thread_id) = ThreadId::from_string(&params.thread_id)
&& let Ok(existing_thread) = self.thread_manager.get_thread(existing_thread_id).await
{
let source_thread = self
.read_stored_thread_for_resume(
&params.thread_id,
/*path*/ None,
/*include_history*/ true,
)
.await?;
if source_thread.thread_id != existing_thread_id {
return Err(invalid_request(format!(
"cannot resume running thread {existing_thread_id} from source thread {}",
source_thread.thread_id
)));
}
Some((existing_thread_id, existing_thread, source_thread))
} else {
None
};
if let Some((existing_thread_id, existing_thread, source_thread)) = running_thread {
let history_items = source_thread
.history
.as_ref()
@@ -4731,11 +4741,10 @@ impl CodexMessageProcessor {
async fn read_stored_thread_for_new_fork(
&self,
thread_store: &dyn ThreadStore,
thread_id: ThreadId,
include_history: bool,
) -> Result<StoredThread, JSONRPCErrorError> {
thread_store
self.thread_store
.read_thread(StoreReadThreadParams {
thread_id,
include_archived: true,
@@ -4938,7 +4947,6 @@ impl CodexMessageProcessor {
let fallback_model_provider = config.model_provider_id.clone();
let instruction_sources = Self::instruction_sources_from_config(&config).await;
let fork_thread_store = thread_store_from_config(&config);
let NewThread {
thread_id,
@@ -4950,7 +4958,6 @@ impl CodexMessageProcessor {
.fork_thread_from_history(
ForkSnapshot::Interrupted,
config,
fork_thread_store.clone(),
InitialHistory::Resumed(ResumedHistory {
conversation_id: source_thread_id,
history: history_items.clone(),
@@ -4986,11 +4993,7 @@ impl CodexMessageProcessor {
let mut thread =
if let Some(fork_rollout_path) = session_configured.rollout_path.as_ref() {
let stored_thread = self
.read_stored_thread_for_new_fork(
fork_thread_store.as_ref(),
thread_id,
include_turns,
)
.read_stored_thread_for_new_fork(thread_id, include_turns)
.await?;
self.stored_thread_to_api_thread(
stored_thread,
@@ -7250,7 +7253,6 @@ impl CodexMessageProcessor {
.fork_thread(
ForkSnapshot::Interrupted,
config.clone(),
thread_store_from_config(&config),
rollout_path,
/*persist_extended_history*/ false,
self.request_trace_context(request_id).await,
@@ -65,6 +65,7 @@ use codex_arg0::Arg0DispatchPaths;
use codex_chatgpt::connectors;
use codex_core::ThreadManager;
use codex_core::config::Config;
use codex_core::thread_store_from_config;
use codex_exec_server::EnvironmentManager;
use codex_features::Feature;
use codex_feedback::CodexFeedback;
@@ -285,12 +286,17 @@ impl MessageProcessor {
auth_manager.set_external_auth(Arc::new(ExternalAuthRefreshBridge {
outgoing: outgoing.clone(),
}));
// The thread store is intentionally process-scoped. Config reloads can
// affect per-thread behavior, but they must not move newly started,
// resumed, or forked threads to a different persistence backend/root.
let thread_store = thread_store_from_config(config.as_ref());
let thread_manager = Arc::new(ThreadManager::new(
config.as_ref(),
auth_manager.clone(),
session_source,
environment_manager,
Some(analytics_events_client.clone()),
Arc::clone(&thread_store),
));
thread_manager
.plugins_manager()
@@ -304,6 +310,7 @@ impl MessageProcessor {
arg0_paths,
config: Arc::clone(&config),
config_manager: config_manager.clone(),
thread_store,
feedback,
log_db,
});