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
+67 -48
View File
@@ -16,8 +16,10 @@ use codex_state::ThreadMetadata;
use super::LocalThreadStore;
use super::helpers::distinct_thread_metadata_title;
use super::helpers::git_info_from_parts;
use super::helpers::rollout_path_is_archived;
use super::helpers::set_thread_name_from_title;
use super::helpers::stored_thread_from_rollout_item;
use super::live_writer;
use crate::ReadThreadParams;
use crate::StoredThread;
use crate::StoredThreadHistory;
@@ -30,7 +32,12 @@ pub(super) async fn read_thread(
) -> ThreadStoreResult<StoredThread> {
let thread_id = params.thread_id;
if let Some(metadata) = read_sqlite_metadata(store, thread_id).await
&& (params.include_archived || metadata.archived_at.is_none())
&& (params.include_archived
|| (metadata.archived_at.is_none()
&& !rollout_path_is_archived(
store.config.codex_home.as_path(),
metadata.rollout_path.as_path(),
)))
&& (!params.include_history
|| sqlite_rollout_path_can_load_history_for_thread(
store,
@@ -44,6 +51,7 @@ pub(super) async fn read_thread(
&& let Some(rollout_path) = thread.rollout_path.clone()
&& let Ok(mut rollout_thread) = read_thread_from_rollout_path(store, rollout_path).await
&& rollout_thread.thread_id == thread_id
&& (params.include_archived || rollout_thread.archived_at.is_none())
&& !rollout_thread.preview.is_empty()
{
if thread.name.is_some() {
@@ -153,6 +161,17 @@ async fn resolve_rollout_path(
thread_id: codex_protocol::ThreadId,
include_archived: bool,
) -> ThreadStoreResult<Option<std::path::PathBuf>> {
if let Ok(path) = live_writer::rollout_path(store, thread_id).await
&& tokio::fs::try_exists(path.as_path()).await.map_err(|err| {
ThreadStoreError::InvalidRequest {
message: format!("failed to check rollout path for thread id {thread_id}: {err}"),
}
})?
&& (include_archived || !rollout_path_is_archived(store.config.codex_home.as_path(), &path))
{
return Ok(Some(path));
}
if include_archived {
match find_thread_path_by_id_str(store.config.codex_home.as_path(), &thread_id.to_string())
.await
@@ -185,21 +204,25 @@ async fn read_thread_from_rollout_path(
let Some(item) = read_thread_item_from_rollout(path.clone()).await else {
return stored_thread_from_session_meta(store, path).await;
};
let archived = path.starts_with(
store
.config
.codex_home
.join(codex_rollout::ARCHIVED_SESSIONS_SUBDIR),
);
let mut thread =
stored_thread_from_rollout_item(item, archived, store.config.model_provider_id.as_str())
.ok_or_else(|| ThreadStoreError::Internal {
message: format!("failed to read thread id from {}", path.display()),
})?;
thread.forked_from_id = read_session_meta_line(path.as_path())
.await
.ok()
.and_then(|meta_line| meta_line.meta.forked_from_id);
let archived = rollout_path_is_archived(store.config.codex_home.as_path(), path.as_path());
let mut thread = stored_thread_from_rollout_item(
item,
archived,
store.config.default_model_provider_id.as_str(),
)
.ok_or_else(|| ThreadStoreError::Internal {
message: format!("failed to read thread id from {}", path.display()),
})?;
if let Ok(meta_line) = read_session_meta_line(path.as_path()).await {
thread.forked_from_id = meta_line.meta.forked_from_id;
if let Some(model_provider) = meta_line
.meta
.model_provider
.filter(|provider| !provider.is_empty())
{
thread.model_provider = model_provider;
}
}
if let Ok(Some(title)) =
find_thread_name_by_id(store.config.codex_home.as_path(), &thread.thread_id).await
{
@@ -225,7 +248,7 @@ async fn read_sqlite_metadata(
) -> Option<ThreadMetadata> {
let runtime = StateRuntime::init(
store.config.sqlite_home.clone(),
store.config.model_provider_id.clone(),
store.config.default_model_provider_id.clone(),
)
.await
.ok()?;
@@ -254,7 +277,7 @@ async fn stored_thread_from_sqlite_metadata(
preview: metadata.first_user_message.clone().unwrap_or_default(),
name,
model_provider: if metadata.model_provider.is_empty() {
store.config.model_provider_id.clone()
store.config.default_model_provider_id.clone()
} else {
metadata.model_provider
},
@@ -294,12 +317,7 @@ async fn stored_thread_from_session_meta(
.map_err(|err| ThreadStoreError::Internal {
message: format!("failed to read thread {}: {err}", path.display()),
})?;
let archived = path.starts_with(
store
.config
.codex_home
.join(codex_rollout::ARCHIVED_SESSIONS_SUBDIR),
);
let archived = rollout_path_is_archived(store.config.codex_home.as_path(), path.as_path());
Ok(stored_thread_from_meta_line(
store, meta_line, path, archived,
))
@@ -327,7 +345,7 @@ fn stored_thread_from_meta_line(
.meta
.model_provider
.filter(|provider| !provider.is_empty())
.unwrap_or_else(|| store.config.model_provider_id.clone()),
.unwrap_or_else(|| store.config.default_model_provider_id.clone()),
model: None,
reasoning_effort: None,
created_at,
@@ -459,7 +477,7 @@ mod tests {
write_session_file(home.path(), "2025-01-03T12-00-00", uuid).expect("session file");
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.model_provider_id.clone(),
config.default_model_provider_id.clone(),
)
.await
.expect("state db should initialize");
@@ -469,10 +487,10 @@ mod tests {
Utc::now(),
SessionSource::Cli,
);
builder.model_provider = Some(config.model_provider_id.clone());
builder.model_provider = Some(config.default_model_provider_id.clone());
builder.git_branch = Some("sqlite-branch".to_string());
runtime
.upsert_thread(&builder.build(config.model_provider_id.as_str()))
.upsert_thread(&builder.build(config.default_model_provider_id.as_str()))
.await
.expect("state db upsert should succeed");
@@ -606,16 +624,16 @@ mod tests {
write_session_file(home.path(), "2025-01-03T12-00-00", uuid).expect("session file");
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.model_provider_id.clone(),
config.default_model_provider_id.clone(),
)
.await
.expect("state db should initialize");
let mut builder =
ThreadMetadataBuilder::new(thread_id, rollout_path, Utc::now(), SessionSource::Cli);
builder.model_provider = Some(config.model_provider_id.clone());
builder.model_provider = Some(config.default_model_provider_id.clone());
builder.cwd = home.path().to_path_buf();
builder.cli_version = Some("test_version".to_string());
let mut metadata = builder.build(config.model_provider_id.as_str());
let mut metadata = builder.build(config.default_model_provider_id.as_str());
metadata.title = "Saved title".to_string();
metadata.first_user_message = Some("Hello from user".to_string());
runtime
@@ -674,7 +692,7 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.model_provider_id.clone(),
config.default_model_provider_id.clone(),
)
.await
.expect("state db should initialize");
@@ -684,9 +702,9 @@ mod tests {
Utc::now(),
SessionSource::Cli,
);
builder.model_provider = Some(config.model_provider_id.clone());
builder.model_provider = Some(config.default_model_provider_id.clone());
builder.cwd = home.path().join("sqlite-workspace");
let mut metadata = builder.build(config.model_provider_id.as_str());
let mut metadata = builder.build(config.default_model_provider_id.as_str());
metadata.title = "Saved title".to_string();
metadata.first_user_message = Some("Hello from sqlite".to_string());
runtime
@@ -707,6 +725,7 @@ mod tests {
assert_eq!(thread.rollout_path, Some(rollout_path));
assert_eq!(thread.preview, "Hello from rollout");
assert_eq!(thread.name, Some("Saved title".to_string()));
assert_eq!(thread.model_provider, "rollout-provider");
assert_eq!(thread.cwd, rollout_cwd);
}
@@ -761,7 +780,7 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.model_provider_id.clone(),
config.default_model_provider_id.clone(),
)
.await
.expect("state db should initialize");
@@ -774,7 +793,7 @@ mod tests {
builder.model_provider = Some("sqlite-provider".to_string());
builder.cwd = home.path().join("workspace");
builder.cli_version = Some("sqlite-cli".to_string());
let mut metadata = builder.build(config.model_provider_id.as_str());
let mut metadata = builder.build(config.default_model_provider_id.as_str());
metadata.title = "Command-only thread".to_string();
runtime
.upsert_thread(&metadata)
@@ -815,7 +834,7 @@ mod tests {
let stale_path = external.path().join("missing-rollout.jsonl");
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.model_provider_id.clone(),
config.default_model_provider_id.clone(),
)
.await
.expect("state db should initialize");
@@ -826,7 +845,7 @@ mod tests {
SessionSource::Cli,
);
builder.model_provider = Some("stale-sqlite-provider".to_string());
let mut metadata = builder.build(config.model_provider_id.as_str());
let mut metadata = builder.build(config.default_model_provider_id.as_str());
metadata.first_user_message = Some("stale sqlite preview".to_string());
runtime
.upsert_thread(&metadata)
@@ -845,7 +864,7 @@ mod tests {
assert_eq!(thread.thread_id, thread_id);
assert_eq!(thread.rollout_path, Some(rollout_path));
assert_eq!(thread.preview, "Hello from user");
assert_eq!(thread.model_provider, config.model_provider_id);
assert_eq!(thread.model_provider, config.default_model_provider_id);
let history = thread.history.expect("history should load");
assert_eq!(history.thread_id, thread_id);
assert_eq!(history.items.len(), 2);
@@ -866,14 +885,14 @@ mod tests {
.expect("other session file");
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.model_provider_id.clone(),
config.default_model_provider_id.clone(),
)
.await
.expect("state db should initialize");
let mut builder =
ThreadMetadataBuilder::new(thread_id, stale_path, Utc::now(), SessionSource::Cli);
builder.model_provider = Some("wrong-sqlite-provider".to_string());
let mut metadata = builder.build(config.model_provider_id.as_str());
let mut metadata = builder.build(config.default_model_provider_id.as_str());
metadata.first_user_message = Some("wrong sqlite preview".to_string());
runtime
.upsert_thread(&metadata)
@@ -892,7 +911,7 @@ mod tests {
assert_eq!(thread.thread_id, thread_id);
assert_eq!(thread.rollout_path, Some(rollout_path));
assert_eq!(thread.preview, "Hello from user");
assert_eq!(thread.model_provider, config.model_provider_id);
assert_eq!(thread.model_provider, config.default_model_provider_id);
let history = thread.history.expect("history should load");
assert_eq!(history.thread_id, thread_id);
assert_eq!(history.items.len(), 2);
@@ -964,7 +983,7 @@ mod tests {
.join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl"));
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.model_provider_id.clone(),
config.default_model_provider_id.clone(),
)
.await
.expect("state db should initialize");
@@ -977,7 +996,7 @@ mod tests {
builder.model_provider = Some("sqlite-provider".to_string());
builder.cwd = external.path().join("workspace");
builder.cli_version = Some("sqlite-cli".to_string());
let mut metadata = builder.build(config.model_provider_id.as_str());
let mut metadata = builder.build(config.default_model_provider_id.as_str());
metadata.title = "SQLite title".to_string();
metadata.first_user_message = Some("SQLite preview".to_string());
metadata.model = Some("sqlite-model".to_string());
@@ -1022,14 +1041,14 @@ mod tests {
.join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl"));
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.model_provider_id.clone(),
config.default_model_provider_id.clone(),
)
.await
.expect("state db should initialize");
let mut builder =
ThreadMetadataBuilder::new(thread_id, rollout_path, Utc::now(), SessionSource::Cli);
builder.archived_at = Some(Utc::now());
let mut metadata = builder.build(config.model_provider_id.as_str());
let mut metadata = builder.build(config.default_model_provider_id.as_str());
metadata.first_user_message = Some("Archived SQLite preview".to_string());
runtime
.upsert_thread(&metadata)
@@ -1077,7 +1096,7 @@ mod tests {
.expect("archived session file");
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.model_provider_id.clone(),
config.default_model_provider_id.clone(),
)
.await
.expect("state db should initialize");
@@ -1088,7 +1107,7 @@ mod tests {
SessionSource::Cli,
);
builder.archived_at = Some(Utc::now());
let mut metadata = builder.build(config.model_provider_id.as_str());
let mut metadata = builder.build(config.default_model_provider_id.as_str());
metadata.first_user_message = Some("Archived SQLite preview".to_string());
runtime
.upsert_thread(&metadata)