mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user