Add SQLite init and fallback telemetry

This commit is contained in:
Owen Lin
2026-05-08 12:42:52 -07:00
parent cf941ede15
commit 33b182d805
98 changed files with 2505 additions and 873 deletions
+7 -4
View File
@@ -125,10 +125,13 @@ mod tests {
async fn state_runtime() -> TestRuntime {
let codex_home = TempDir::new().expect("tempdir should be created");
let state_db =
StateRuntime::init(codex_home.path().to_path_buf(), "test-provider".to_string())
.await
.expect("state db should initialize");
let state_db = StateRuntime::init(
codex_home.path().to_path_buf(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
TestRuntime {
state_db,
_codex_home: codex_home,
+8 -7
View File
@@ -29,6 +29,7 @@ pub use codex_app_server::in_process::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY;
pub use codex_app_server::in_process::InProcessServerEvent;
use codex_app_server::in_process::InProcessStartArgs;
use codex_app_server::in_process::LogDbLayer;
pub use codex_app_server::in_process::StateDbAccess;
pub use codex_app_server::in_process::StateDbHandle;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::ClientNotification;
@@ -340,8 +341,8 @@ pub struct InProcessClientStartArgs {
pub feedback: CodexFeedback,
/// SQLite tracing layer used to flush recently emitted logs before feedback upload.
pub log_db: Option<LogDbLayer>,
/// Process-wide SQLite state handle shared with the embedded app-server.
pub state_db: Option<StateDbHandle>,
/// Process-wide SQLite access shared with the embedded app-server.
pub state_db_access: StateDbAccess,
/// Environment manager used by core execution and filesystem operations.
pub environment_manager: Arc<EnvironmentManager>,
/// Startup warnings emitted after initialize succeeds.
@@ -404,7 +405,7 @@ impl InProcessClientStartArgs {
thread_config_loader,
feedback: self.feedback,
log_db: self.log_db,
state_db: self.state_db,
state_db_access: self.state_db_access,
environment_manager: self.environment_manager,
config_warnings: self.config_warnings,
session_source: self.session_source,
@@ -1017,7 +1018,7 @@ mod tests {
) -> TestClient {
let codex_home = TempDir::new().expect("temp dir");
let config = Arc::new(build_test_config_for_codex_home(codex_home.path()).await);
let state_db = init_state_db(config.as_ref())
let state_db = init_state_db(config.as_ref(), /*metrics*/ None)
.await
.expect("state db should initialize for in-process test");
let client = InProcessAppServerClient::start(InProcessClientStartArgs {
@@ -1028,7 +1029,7 @@ mod tests {
cloud_requirements: CloudRequirementsLoader::default(),
feedback: CodexFeedback::new(),
log_db: None,
state_db: Some(state_db),
state_db_access: StateDbAccess::new(Some(state_db)),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
config_warnings: Vec::new(),
session_source,
@@ -2112,7 +2113,7 @@ mod tests {
cloud_requirements: CloudRequirementsLoader::default(),
feedback: CodexFeedback::new(),
log_db: None,
state_db: None,
state_db_access: StateDbAccess::none(),
environment_manager: environment_manager.clone(),
config_warnings: Vec::new(),
session_source: SessionSource::Exec,
@@ -2152,7 +2153,7 @@ mod tests {
cloud_requirements: CloudRequirementsLoader::default(),
feedback: CodexFeedback::new(),
log_db: None,
state_db: None,
state_db_access: StateDbAccess::none(),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
config_warnings: Vec::new(),
session_source: SessionSource::Exec,
@@ -277,9 +277,13 @@ mod tests {
use tokio::time::timeout;
async fn remote_control_state_runtime(codex_home: &TempDir) -> Arc<StateRuntime> {
StateRuntime::init(codex_home.path().to_path_buf(), "test-provider".to_string())
.await
.expect("state runtime should initialize")
StateRuntime::init(
codex_home.path().to_path_buf(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state runtime should initialize")
}
#[tokio::test]
@@ -109,9 +109,13 @@ fn remote_control_auth_dot_json(account_id: Option<&str>) -> AuthDotJson {
}
async fn remote_control_state_runtime(codex_home: &TempDir) -> Arc<StateRuntime> {
StateRuntime::init(codex_home.path().to_path_buf(), "test-provider".to_string())
.await
.expect("state runtime should initialize")
StateRuntime::init(
codex_home.path().to_path_buf(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state runtime should initialize")
}
fn remote_control_url_for_listener(listener: &TcpListener) -> String {
@@ -1286,9 +1286,13 @@ mod tests {
}
async fn remote_control_state_runtime(codex_home: &TempDir) -> Arc<StateRuntime> {
StateRuntime::init(codex_home.path().to_path_buf(), "test-provider".to_string())
.await
.expect("state runtime should initialize")
StateRuntime::init(
codex_home.path().to_path_buf(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state runtime should initialize")
}
fn remote_control_auth_manager() -> Arc<AuthManager> {
+7 -5
View File
@@ -86,7 +86,9 @@ use codex_exec_server::EnvironmentManager;
use codex_feedback::CodexFeedback;
use codex_login::AuthManager;
use codex_protocol::protocol::SessionSource;
pub use codex_rollout::StateDbAccess;
pub use codex_rollout::StateDbHandle;
pub use codex_state::DbMetricsRecorderHandle;
pub use codex_state::log_db::LogDbLayer;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
@@ -127,8 +129,8 @@ pub struct InProcessStartArgs {
pub feedback: CodexFeedback,
/// SQLite tracing layer used to flush recently emitted logs before feedback upload.
pub log_db: Option<LogDbLayer>,
/// Process-wide SQLite state handle shared with embedded app-server consumers.
pub state_db: Option<StateDbHandle>,
/// Process-wide SQLite access shared with embedded app-server consumers.
pub state_db_access: StateDbAccess,
/// Environment manager used by core execution and filesystem operations.
pub environment_manager: Arc<EnvironmentManager>,
/// Startup warnings emitted after initialize succeeds.
@@ -424,7 +426,7 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
environment_manager: args.environment_manager,
feedback: args.feedback,
log_db: args.log_db,
state_db: args.state_db,
state_db_access: args.state_db_access,
config_warnings: args.config_warnings,
session_source: args.session_source,
auth_manager,
@@ -757,7 +759,7 @@ mod tests {
) -> InProcessClientHandle {
let codex_home = TempDir::new().expect("temp dir");
let config = Arc::new(build_test_config(codex_home.path()).await);
let state_db = codex_rollout::state_db::try_init(config.as_ref())
let state_db = codex_rollout::state_db::try_init(config.as_ref(), /*metrics*/ None)
.await
.expect("state db should initialize for in-process test");
let args = InProcessStartArgs {
@@ -769,7 +771,7 @@ mod tests {
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
log_db: None,
state_db: Some(state_db),
state_db_access: StateDbAccess::new(Some(state_db)),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
config_warnings: Vec::new(),
session_source,
+22 -16
View File
@@ -107,6 +107,7 @@ pub use crate::transport::auth::AppServerWebsocketAuthSettings;
pub use crate::transport::auth::WebsocketAuthCliMode;
const LOG_FORMAT_ENV_VAR: &str = "LOG_FORMAT";
const OTEL_SERVICE_NAME: &str = "codex-app-server";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum LogFormat {
@@ -492,9 +493,27 @@ pub async fn run_main_with_transport_options(
}
};
let state_db_result = rollout_state_db::try_init(&config).await;
let otel = codex_core::otel_init::build_provider(
&config,
env!("CARGO_PKG_VERSION"),
Some(OTEL_SERVICE_NAME),
default_analytics_enabled,
)
.map_err(|e| {
std::io::Error::new(
ErrorKind::InvalidData,
format!("error loading otel config: {e}"),
)
})?;
codex_core::otel_init::record_process_start(otel.as_ref(), OTEL_SERVICE_NAME);
let state_db_metrics =
codex_core::otel_init::sqlite_metrics_recorder(otel.as_ref(), OTEL_SERVICE_NAME);
let state_db_result = rollout_state_db::try_init(&config, state_db_metrics.clone()).await;
let state_db_init_error = state_db_result.as_ref().err().map(ToString::to_string);
let state_db = state_db_result.ok();
let state_db_access = codex_rollout::StateDbAccess::new(state_db.clone())
.with_standalone_metrics(state_db_metrics);
if should_run_personality_migration {
let effective_toml = config.config_layer_stack.effective_config();
@@ -503,7 +522,7 @@ pub async fn run_main_with_transport_options(
match codex_core::personality_migration::maybe_migrate_personality(
&config.codex_home,
&config_toml,
state_db.clone(),
state_db_access.clone(),
)
.await
{
@@ -571,19 +590,6 @@ pub async fn run_main_with_transport_options(
let feedback = CodexFeedback::new();
let otel = codex_core::otel_init::build_provider(
&config,
env!("CARGO_PKG_VERSION"),
Some("codex-app-server"),
default_analytics_enabled,
)
.map_err(|e| {
std::io::Error::new(
ErrorKind::InvalidData,
format!("error loading otel config: {e}"),
)
})?;
// Install a simple subscriber so `tracing` output is visible. Users can
// control the log level with `RUST_LOG` and switch to JSON logs with
// `LOG_FORMAT=json`.
@@ -778,7 +784,7 @@ pub async fn run_main_with_transport_options(
environment_manager,
feedback: feedback.clone(),
log_db,
state_db: state_db.clone(),
state_db_access,
config_warnings,
session_source,
auth_manager,
+4 -3
View File
@@ -174,10 +174,11 @@ mod tests {
.await?;
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy"));
let state_db = init_state_db(&good_config)
let state_db = init_state_db(&good_config, /*metrics*/ None)
.await
.expect("refresh tests require state db");
let thread_store = thread_store_from_config(&good_config, Some(state_db.clone()));
let state_db_access = codex_core::StateDbAccess::new(Some(state_db.clone()));
let thread_store = thread_store_from_config(&good_config, state_db_access.clone());
let thread_manager = Arc::new(ThreadManager::new(
&good_config,
auth_manager,
@@ -185,7 +186,7 @@ mod tests {
Arc::new(EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
thread_store,
Some(state_db.clone()),
state_db_access,
"11111111-1111-4111-8111-111111111111".to_string(),
/*attestation_provider*/ None,
));
+7 -6
View File
@@ -73,7 +73,7 @@ use codex_login::auth::ExternalAuthTokens;
use codex_protocol::ThreadId;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::W3cTraceContext;
use codex_rollout::StateDbHandle;
use codex_rollout::StateDbAccess;
use codex_state::log_db::LogDbLayer;
use tokio::sync::Mutex;
use tokio::sync::Semaphore;
@@ -255,7 +255,7 @@ pub(crate) struct MessageProcessorArgs {
pub(crate) environment_manager: Arc<EnvironmentManager>,
pub(crate) feedback: CodexFeedback,
pub(crate) log_db: Option<LogDbLayer>,
pub(crate) state_db: Option<StateDbHandle>,
pub(crate) state_db_access: StateDbAccess,
pub(crate) config_warnings: Vec<ConfigWarningNotification>,
pub(crate) session_source: SessionSource,
pub(crate) auth_manager: Arc<AuthManager>,
@@ -278,7 +278,7 @@ impl MessageProcessor {
environment_manager,
feedback,
log_db,
state_db,
state_db_access,
config_warnings,
session_source,
auth_manager,
@@ -294,7 +294,8 @@ impl MessageProcessor {
// 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(), state_db.clone());
let state_db = state_db_access.state_db();
let thread_store = thread_store_from_config(config.as_ref(), state_db_access.clone());
let thread_manager = Arc::new(ThreadManager::new(
config.as_ref(),
auth_manager.clone(),
@@ -302,7 +303,7 @@ impl MessageProcessor {
environment_manager,
Some(analytics_events_client.clone()),
Arc::clone(&thread_store),
state_db.clone(),
state_db_access.clone(),
installation_id,
Some(app_server_attestation_provider(
outgoing.clone(),
@@ -387,7 +388,7 @@ impl MessageProcessor {
outgoing.clone(),
Arc::clone(&config),
thread_state_manager.clone(),
state_db.clone(),
state_db_access,
);
let thread_processor = ThreadRequestProcessor::new(
auth_manager.clone(),
@@ -258,7 +258,7 @@ async fn build_test_processor(
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
feedback: CodexFeedback::new(),
log_db: None,
state_db: None,
state_db_access: codex_rollout::StateDbAccess::none(),
config_warnings: Vec::new(),
session_source: SessionSource::VSCode,
auth_manager,
@@ -262,6 +262,7 @@ use codex_core::NewThread;
#[cfg(test)]
use codex_core::SessionMeta;
use codex_core::StartThreadOptions;
use codex_core::StateDbAccess;
use codex_core::SteerInputError;
use codex_core::ThreadConfigSnapshot;
use codex_core::ThreadManager;
@@ -7,7 +7,7 @@ pub(crate) struct ThreadGoalRequestProcessor {
outgoing: Arc<OutgoingMessageSender>,
config: Arc<Config>,
thread_state_manager: ThreadStateManager,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
}
impl ThreadGoalRequestProcessor {
@@ -16,14 +16,14 @@ impl ThreadGoalRequestProcessor {
outgoing: Arc<OutgoingMessageSender>,
config: Arc<Config>,
thread_state_manager: ThreadStateManager,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
) -> Self {
Self {
thread_manager,
outgoing,
config,
thread_state_manager,
state_db,
state_db_access,
}
}
@@ -81,7 +81,7 @@ impl ThreadGoalRequestProcessor {
if let Some(state_db) = thread.state_db() {
Some(state_db)
} else {
self.state_db.clone()
self.state_db_access.state_db()
}
} else {
None
@@ -110,7 +110,7 @@ impl ThreadGoalRequestProcessor {
None => find_thread_path_by_id_str(
&self.config.codex_home,
&thread_id.to_string(),
self.state_db.as_deref(),
&self.state_db_access,
)
.await
.map_err(|err| {
@@ -275,7 +275,7 @@ impl ThreadGoalRequestProcessor {
None => find_thread_path_by_id_str(
&self.config.codex_home,
&thread_id.to_string(),
self.state_db.as_deref(),
&self.state_db_access,
)
.await
.map_err(|err| {
@@ -339,7 +339,7 @@ impl ThreadGoalRequestProcessor {
find_thread_path_by_id_str(
&self.config.codex_home,
&thread_id.to_string(),
self.state_db.as_deref(),
&self.state_db_access,
)
.await
.map_err(|err| {
@@ -348,8 +348,8 @@ impl ThreadGoalRequestProcessor {
.ok_or_else(|| invalid_request(format!("thread not found: {thread_id}")))?;
}
self.state_db
.clone()
self.state_db_access
.state_db()
.ok_or_else(|| internal_error("sqlite state db unavailable for thread goals"))
}
@@ -153,7 +153,7 @@ async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() ->
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
log_db: None,
state_db: None,
state_db_access: in_process::StateDbAccess::none(),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
config_warnings: Vec::new(),
session_source: SessionSource::Cli,
@@ -204,7 +204,7 @@ async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()> {
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
log_db: None,
state_db: None,
state_db_access: in_process::StateDbAccess::none(),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
config_warnings: Vec::new(),
session_source: SessionSource::Cli,
@@ -113,7 +113,12 @@ async fn seed_stage1_output(state_db: &Arc<StateRuntime>, codex_home: &Path) ->
}
async fn init_state_db(codex_home: &Path) -> Result<Arc<StateRuntime>> {
let state_db = StateRuntime::init(codex_home.to_path_buf(), "mock_provider".into()).await?;
let state_db = StateRuntime::init(
codex_home.to_path_buf(),
"mock_provider".into(),
/*metrics*/ None,
)
.await?;
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await?;
@@ -79,7 +79,7 @@ async fn thread_start_with_non_local_thread_store_does_not_create_local_persiste
thread_config_loader: Arc::new(NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
log_db: None,
state_db: None,
state_db_access: in_process::StateDbAccess::none(),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
config_warnings: Vec::new(),
session_source: SessionSource::Cli,
@@ -63,9 +63,13 @@ async fn thread_archive_requires_materialized_rollout() -> Result<()> {
rollout_path.display()
);
assert!(
find_thread_path_by_id_str(codex_home.path(), &thread.id, /*state_db_ctx*/ None)
.await?
.is_none(),
find_thread_path_by_id_str(
codex_home.path(),
&thread.id,
&codex_core::StateDbAccess::none()
)
.await?
.is_none(),
"thread id should not be discoverable before rollout materialization"
);
@@ -118,10 +122,13 @@ async fn thread_archive_requires_materialized_rollout() -> Result<()> {
rollout_path.display()
);
let discovered_path =
find_thread_path_by_id_str(codex_home.path(), &thread.id, /*state_db_ctx*/ None)
.await?
.expect("expected rollout path for thread id to exist after materialization");
let discovered_path = find_thread_path_by_id_str(
codex_home.path(),
&thread.id,
&codex_core::StateDbAccess::none(),
)
.await?
.expect("expected rollout path for thread id to exist after materialization");
assert_paths_match_on_disk(&discovered_path, &rollout_path)?;
let archive_id = mcp
@@ -200,8 +207,12 @@ async fn thread_archive_archives_spawned_descendants() -> Result<()> {
let parent_thread_id = ThreadId::from_string(&parent_id)?;
let child_thread_id = ThreadId::from_string(&child_id)?;
let grandchild_thread_id = ThreadId::from_string(&grandchild_id)?;
let state_db =
StateRuntime::init(codex_home.path().to_path_buf(), "mock_provider".into()).await?;
let state_db = StateRuntime::init(
codex_home.path().to_path_buf(),
"mock_provider".into(),
/*metrics*/ None,
)
.await?;
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await?;
@@ -256,7 +267,7 @@ async fn thread_archive_archives_spawned_descendants() -> Result<()> {
find_thread_path_by_id_str(
codex_home.path(),
&thread_id.to_string(),
/*state_db_ctx*/ None,
&codex_core::StateDbAccess::none(),
)
.await?
.is_none(),
@@ -266,7 +277,7 @@ async fn thread_archive_archives_spawned_descendants() -> Result<()> {
find_archived_thread_path_by_id_str(
codex_home.path(),
&thread_id.to_string(),
/*state_db_ctx*/ None,
&codex_core::StateDbAccess::none(),
)
.await?
.is_some(),
@@ -311,8 +322,12 @@ async fn thread_archive_succeeds_when_descendant_archive_fails() -> Result<()> {
let parent_thread_id = ThreadId::from_string(&parent_id)?;
let child_thread_id = ThreadId::from_string(&child_id)?;
let grandchild_thread_id = ThreadId::from_string(&grandchild_id)?;
let state_db =
StateRuntime::init(codex_home.path().to_path_buf(), "mock_provider".into()).await?;
let state_db = StateRuntime::init(
codex_home.path().to_path_buf(),
"mock_provider".into(),
/*metrics*/ None,
)
.await?;
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await?;
@@ -331,10 +346,13 @@ async fn thread_archive_succeeds_when_descendant_archive_fails() -> Result<()> {
)
.await?;
let child_rollout_path =
find_thread_path_by_id_str(codex_home.path(), &child_id, /*state_db_ctx*/ None)
.await?
.expect("child rollout path");
let child_rollout_path = find_thread_path_by_id_str(
codex_home.path(),
&child_id,
&codex_core::StateDbAccess::none(),
)
.await?
.expect("child rollout path");
let archived_child_path = codex_home
.path()
.join(ARCHIVED_SESSIONS_SUBDIR)
@@ -394,7 +412,7 @@ async fn thread_archive_succeeds_when_descendant_archive_fails() -> Result<()> {
find_thread_path_by_id_str(
codex_home.path(),
&thread_id.to_string(),
/*state_db_ctx*/ None,
&codex_core::StateDbAccess::none(),
)
.await?
.is_none(),
@@ -404,7 +422,7 @@ async fn thread_archive_succeeds_when_descendant_archive_fails() -> Result<()> {
find_archived_thread_path_by_id_str(
codex_home.path(),
&thread_id.to_string(),
/*state_db_ctx*/ None,
&codex_core::StateDbAccess::none(),
)
.await?
.is_some(),
@@ -432,8 +450,12 @@ async fn thread_archive_succeeds_when_spawned_descendant_is_missing() -> Result<
let parent_thread_id = ThreadId::from_string(&parent_id)?;
let missing_child_thread_id = ThreadId::from_string("00000000-0000-0000-0000-000000000901")?;
let state_db =
StateRuntime::init(codex_home.path().to_path_buf(), "mock_provider".into()).await?;
let state_db = StateRuntime::init(
codex_home.path().to_path_buf(),
"mock_provider".into(),
/*metrics*/ None,
)
.await?;
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await?;
@@ -473,16 +495,20 @@ async fn thread_archive_succeeds_when_spawned_descendant_is_missing() -> Result<
assert_eq!(archived_notification.thread_id, parent_id);
assert!(
find_thread_path_by_id_str(codex_home.path(), &parent_id, /*state_db_ctx*/ None)
.await?
.is_none(),
find_thread_path_by_id_str(
codex_home.path(),
&parent_id,
&codex_core::StateDbAccess::none()
)
.await?
.is_none(),
"parent should be archived even when a descendant is missing"
);
assert!(
find_archived_thread_path_by_id_str(
codex_home.path(),
&parent_id,
/*state_db_ctx*/ None,
&codex_core::StateDbAccess::none(),
)
.await?
.is_some(),
@@ -600,9 +600,12 @@ sqlite = true
// `thread/list` applies `search_term` on the sqlite fast path. This test creates
// rollouts manually, so mark the DB backfill complete and then run an unsearched
// list large enough to repair every rollout the searched list should find.
let state_db =
codex_state::StateRuntime::init(codex_home.path().to_path_buf(), "mock_provider".into())
.await?;
let state_db = codex_state::StateRuntime::init(
codex_home.path().to_path_buf(),
"mock_provider".into(),
/*metrics*/ None,
)
.await?;
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await?;
@@ -614,7 +617,7 @@ sqlite = true
generate_memories: false,
};
let repaired_page = codex_core::RolloutRecorder::list_threads(
Some(state_db.clone()),
codex_core::StateDbAccess::new(Some(state_db.clone())),
&rollout_config,
/*page_size*/ 10,
/*cursor*/ None,
@@ -683,9 +686,12 @@ sqlite = true
Some("mock_provider"),
/*git_info*/ None,
)?;
let state_db =
codex_state::StateRuntime::init(codex_home.path().to_path_buf(), "mock_provider".into())
.await?;
let state_db = codex_state::StateRuntime::init(
codex_home.path().to_path_buf(),
"mock_provider".into(),
/*metrics*/ None,
)
.await?;
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await?;
@@ -103,7 +103,12 @@ async fn thread_memory_mode_set_updates_stored_thread_state() -> Result<()> {
}
async fn init_state_db(codex_home: &Path) -> Result<Arc<StateRuntime>> {
let state_db = StateRuntime::init(codex_home.to_path_buf(), "mock_provider".into()).await?;
let state_db = StateRuntime::init(
codex_home.to_path_buf(),
"mock_provider".into(),
/*metrics*/ None,
)
.await?;
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await?;
@@ -486,7 +486,12 @@ async fn thread_metadata_update_can_clear_stored_git_fields() -> Result<()> {
}
async fn init_state_db(codex_home: &Path) -> Result<Arc<StateRuntime>> {
let state_db = StateRuntime::init(codex_home.to_path_buf(), "mock_provider".into()).await?;
let state_db = StateRuntime::init(
codex_home.to_path_buf(),
"mock_provider".into(),
/*metrics*/ None,
)
.await?;
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await?;
@@ -379,7 +379,7 @@ async fn thread_turns_list_reads_store_history_without_rollout_path() -> Result<
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
log_db: None,
state_db: None,
state_db_access: in_process::StateDbAccess::none(),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
config_warnings: Vec::new(),
session_source: SessionSource::Cli.into(),
@@ -444,7 +444,7 @@ async fn thread_read_loaded_include_turns_reads_store_history_without_rollout_pa
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
log_db: None,
state_db: None,
state_db_access: in_process::StateDbAccess::none(),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
config_warnings: Vec::new(),
session_source: SessionSource::Cli.into(),
@@ -529,7 +529,7 @@ async fn thread_list_includes_store_thread_without_rollout_path() -> Result<()>
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
log_db: None,
state_db: None,
state_db_access: in_process::StateDbAccess::none(),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
config_warnings: Vec::new(),
session_source: SessionSource::Cli.into(),
@@ -1223,8 +1223,12 @@ stream_max_retries = 0
.join("\n")
+ "\n",
)?;
let state_db =
StateRuntime::init(codex_home.path().to_path_buf(), "mock_provider".into()).await?;
let state_db = StateRuntime::init(
codex_home.path().to_path_buf(),
"mock_provider".into(),
/*metrics*/ None,
)
.await?;
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await?;
@@ -100,10 +100,13 @@ async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result
)
.await??;
let found_rollout_path =
find_thread_path_by_id_str(codex_home.path(), &thread.id, /*state_db_ctx*/ None)
.await?
.expect("expected rollout path for thread id to exist");
let found_rollout_path = find_thread_path_by_id_str(
codex_home.path(),
&thread.id,
&codex_core::StateDbAccess::none(),
)
.await?
.expect("expected rollout path for thread id to exist");
assert_paths_match_on_disk(&found_rollout_path, &rollout_path)?;
let archive_id = mcp
@@ -121,7 +124,7 @@ async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result
let archived_path = find_archived_thread_path_by_id_str(
codex_home.path(),
&thread.id,
/*state_db_ctx*/ None,
&codex_core::StateDbAccess::none(),
)
.await?
.expect("expected archived rollout path for thread id to exist");
@@ -249,7 +252,7 @@ async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> {
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
log_db: None,
state_db: None,
state_db_access: in_process::StateDbAccess::none(),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
config_warnings: Vec::new(),
session_source: SessionSource::Cli,
+8 -4
View File
@@ -1440,7 +1440,8 @@ async fn run_debug_prompt_input_command(
});
}
let prompt_input = codex_core::build_prompt_input(config, input, /*state_db*/ None).await?;
let prompt_input =
codex_core::build_prompt_input(config, input, codex_core::StateDbAccess::none()).await?;
println!("{}", serde_json::to_string_pretty(&prompt_input)?);
Ok(())
@@ -1487,9 +1488,12 @@ async fn run_debug_clear_memories_command(
let state_path = state_db_path(config.sqlite_home.as_path());
let mut cleared_state_db = false;
if tokio::fs::try_exists(&state_path).await? {
let state_db =
StateRuntime::init(config.sqlite_home.clone(), config.model_provider_id.clone())
.await?;
let state_db = StateRuntime::init(
config.sqlite_home.clone(),
config.model_provider_id.clone(),
/*metrics*/ None,
)
.await?;
state_db.clear_memory_data().await?;
cleared_state_db = true;
}
+6 -2
View File
@@ -16,8 +16,12 @@ fn codex_command(codex_home: &Path) -> Result<assert_cmd::Command> {
#[tokio::test]
async fn debug_clear_memories_resets_state_and_removes_memory_dir() -> Result<()> {
let codex_home = TempDir::new()?;
let runtime =
StateRuntime::init(codex_home.path().to_path_buf(), "test-provider".to_string()).await?;
let runtime = StateRuntime::init(
codex_home.path().to_path_buf(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await?;
drop(runtime);
let thread_id = "00000000-0000-0000-0000-000000000123";
+3 -3
View File
@@ -94,7 +94,7 @@ struct AgentControlHarness {
impl AgentControlHarness {
async fn new() -> Self {
let (home, config) = test_config().await;
let state_db = init_state_db(&config).await;
let state_db = init_state_db(&config, /*metrics*/ None).await;
let manager = ThreadManager::with_models_provider_home_and_state_for_tests(
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
@@ -1543,7 +1543,7 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
.features
.enable(Feature::Sqlite)
.expect("test config should allow sqlite");
let state_db = init_state_db(&config).await;
let state_db = init_state_db(&config, /*metrics*/ None).await;
let manager = ThreadManager::with_models_provider_home_and_state_for_tests(
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
@@ -1706,7 +1706,7 @@ async fn resume_agent_from_rollout_reads_archived_rollout_path() {
.expect("child shutdown should succeed");
let store = LocalThreadStore::new(
LocalThreadStoreConfig::from_config(&harness.config),
harness.state_db.clone(),
crate::StateDbAccess::new(harness.state_db.clone()),
);
store
.archive_thread(ArchiveThreadParams {
+1
View File
@@ -100,6 +100,7 @@ pub(crate) async fn run_codex_thread_interactive(
analytics_events_client: Some(parent_session.services.analytics_events_client.clone()),
thread_store: Arc::clone(&parent_session.services.thread_store),
attestation_provider: parent_session.services.attestation_provider.clone(),
state_db_access: parent_session.services.state_db_access.clone(),
}))
.or_cancel(&cancel_token)
.await??;
+2
View File
@@ -140,6 +140,8 @@ pub mod shell;
pub(crate) mod shell_snapshot;
pub mod spawn;
pub(crate) mod state_db_bridge;
pub use state_db_bridge::DbMetricsRecorderHandle;
pub use state_db_bridge::StateDbAccess;
pub use state_db_bridge::StateDbHandle;
pub use state_db_bridge::init_state_db;
mod thread_rollout_truncation;
+1 -1
View File
@@ -770,7 +770,7 @@ async fn maybe_mark_thread_memory_mode_polluted(
return;
}
state_db::mark_thread_memory_mode_polluted(
sess.services.state_db.as_deref(),
sess.services.state_db_access.as_deref(),
sess.conversation_id,
"mcp_tool_call",
)
+30 -2
View File
@@ -2,7 +2,6 @@ use crate::config::Config;
use codex_config::types::OtelExporterKind as Kind;
use codex_config::types::OtelHttpProtocol as Protocol;
use codex_features::Feature;
use codex_login::default_client::originator;
use codex_otel::OtelExporter;
use codex_otel::OtelHttpProtocol;
use codex_otel::OtelProvider;
@@ -76,7 +75,7 @@ pub fn build_provider(
OtelExporter::None
};
let originator = originator();
let originator = codex_login::default_client::originator();
let service_name = service_name_override.unwrap_or(originator.value.as_str());
let runtime_metrics = config.features.enabled(Feature::RuntimeMetrics);
@@ -94,6 +93,35 @@ pub fn build_provider(
})
}
/// Record the process-start denominator using the caller's explicit process identity.
pub fn record_process_start(provider: Option<&OtelProvider>, originator: &str) {
if let Some(provider) = provider
&& let Some(metrics) = provider.metrics()
{
let _ = codex_otel::record_process_start_once(metrics, originator);
}
}
/// Build a SQLite metrics recorder from the caller's explicit process identity.
pub fn sqlite_metrics_recorder(
provider: Option<&OtelProvider>,
originator: &str,
) -> Option<codex_state::DbMetricsRecorderHandle> {
provider.and_then(|provider| {
provider.metrics().map(|metrics| {
codex_rollout::state_db::otel_db_metrics_recorder(metrics.clone(), originator)
})
})
}
/// Build a SQLite metrics recorder from the process-global OTEL metrics client.
pub fn sqlite_metrics_recorder_from_global(
originator: &str,
) -> Option<codex_state::DbMetricsRecorderHandle> {
codex_otel::global()
.map(|metrics| codex_rollout::state_db::otel_db_metrics_recorder(metrics, originator))
}
/// Filter predicate for exporting only Codex-owned events via OTEL.
/// Keeps events that originated from codex_otel module
pub fn codex_export_filter(meta: &tracing::Metadata<'_>) -> bool {
+5 -5
View File
@@ -1,7 +1,7 @@
use crate::config::edit::ConfigEditsBuilder;
use codex_config::config_toml::ConfigToml;
use codex_protocol::config_types::Personality;
use codex_rollout::state_db::StateDbHandle;
use codex_rollout::StateDbAccess;
use codex_thread_store::ListThreadsParams;
use codex_thread_store::LocalThreadStore;
use codex_thread_store::LocalThreadStoreConfig;
@@ -25,7 +25,7 @@ pub enum PersonalityMigrationStatus {
pub async fn maybe_migrate_personality(
codex_home: &Path,
config_toml: &ConfigToml,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
) -> io::Result<PersonalityMigrationStatus> {
let marker_path = codex_home.join(PERSONALITY_MIGRATION_FILENAME);
if tokio::fs::try_exists(&marker_path).await? {
@@ -45,7 +45,7 @@ pub async fn maybe_migrate_personality(
.or_else(|| config_toml.model_provider.clone())
.unwrap_or_else(|| "openai".to_string());
if !has_recorded_sessions(codex_home, model_provider_id.as_str(), state_db).await? {
if !has_recorded_sessions(codex_home, model_provider_id.as_str(), state_db_access).await? {
create_marker(&marker_path).await?;
return Ok(PersonalityMigrationStatus::SkippedNoSessions);
}
@@ -65,7 +65,7 @@ pub async fn maybe_migrate_personality(
async fn has_recorded_sessions(
codex_home: &Path,
default_provider: &str,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
) -> io::Result<bool> {
let store = LocalThreadStore::new(
LocalThreadStoreConfig {
@@ -73,7 +73,7 @@ async fn has_recorded_sessions(
sqlite_home: codex_home.to_path_buf(),
default_model_provider_id: default_provider.to_string(),
},
state_db,
state_db_access,
);
if has_threads(&store, /*archived*/ false).await? {
return Ok(true);
@@ -88,7 +88,8 @@ async fn applies_when_sessions_exist_and_no_personality() -> io::Result<()> {
write_session_with_user_event(temp.path()).await?;
let config_toml = ConfigToml::default();
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
let status =
maybe_migrate_personality(temp.path(), &config_toml, StateDbAccess::none()).await?;
assert_eq!(status, PersonalityMigrationStatus::Applied);
assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists());
@@ -104,7 +105,8 @@ async fn applies_when_only_archived_sessions_exist_and_no_personality() -> io::R
write_archived_session_with_user_event(temp.path()).await?;
let config_toml = ConfigToml::default();
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
let status =
maybe_migrate_personality(temp.path(), &config_toml, StateDbAccess::none()).await?;
assert_eq!(status, PersonalityMigrationStatus::Applied);
assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists());
@@ -120,7 +122,8 @@ async fn skips_when_marker_exists() -> io::Result<()> {
create_marker(&temp.path().join(PERSONALITY_MIGRATION_FILENAME)).await?;
let config_toml = ConfigToml::default();
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
let status =
maybe_migrate_personality(temp.path(), &config_toml, StateDbAccess::none()).await?;
assert_eq!(status, PersonalityMigrationStatus::SkippedMarker);
assert!(!temp.path().join("config.toml").exists());
@@ -137,7 +140,8 @@ async fn skips_when_personality_explicit() -> io::Result<()> {
.map_err(|err| io::Error::other(format!("failed to write config: {err}")))?;
let config_toml = read_config_toml(temp.path()).await?;
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
let status =
maybe_migrate_personality(temp.path(), &config_toml, StateDbAccess::none()).await?;
assert_eq!(
status,
@@ -154,7 +158,8 @@ async fn skips_when_personality_explicit() -> io::Result<()> {
async fn skips_when_no_sessions() -> io::Result<()> {
let temp = TempDir::new()?;
let config_toml = ConfigToml::default();
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
let status =
maybe_migrate_personality(temp.path(), &config_toml, StateDbAccess::none()).await?;
assert_eq!(status, PersonalityMigrationStatus::SkippedNoSessions);
assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists());
+4 -4
View File
@@ -17,7 +17,7 @@ use crate::resolve_installation_id;
use crate::session::session::Session;
use crate::session::turn::build_prompt;
use crate::session::turn::built_tools;
use crate::state_db_bridge::StateDbHandle;
use crate::state_db_bridge::StateDbAccess;
use crate::thread_manager::ThreadManager;
use crate::thread_manager::thread_store_from_config;
@@ -26,7 +26,7 @@ use crate::thread_manager::thread_store_from_config;
pub async fn build_prompt_input(
mut config: Config,
input: Vec<UserInput>,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
) -> CodexResult<Vec<ResponseItem>> {
config.ephemeral = true;
@@ -38,7 +38,7 @@ pub async fn build_prompt_input(
config.codex_linux_sandbox_exe.clone(),
)?;
let thread_store = thread_store_from_config(&config, state_db.clone());
let thread_store = thread_store_from_config(&config, state_db_access.clone());
let installation_id = resolve_installation_id(&config.codex_home).await?;
let thread_manager = ThreadManager::new(
&config,
@@ -51,7 +51,7 @@ pub async fn build_prompt_input(
),
/*analytics_events_client*/ None,
thread_store,
state_db.clone(),
state_db_access,
installation_id,
/*attestation_provider*/ None,
);
+7 -8
View File
@@ -132,7 +132,6 @@ use codex_terminal_detection::user_agent;
use codex_thread_store::CreateThreadParams;
use codex_thread_store::LiveThread;
use codex_thread_store::LiveThreadInitGuard;
use codex_thread_store::LocalThreadStore;
use codex_thread_store::ReadThreadParams;
use codex_thread_store::ResumeThreadParams;
use codex_thread_store::ThreadEventPersistenceMode;
@@ -273,6 +272,7 @@ use crate::SkillLoadOutcome;
#[cfg(test)]
use crate::SkillMetadata;
use crate::SkillsManager;
use crate::StateDbAccess;
use crate::agents_md::AgentsMdManager;
use crate::context::UserInstructions;
use crate::exec_policy::ExecPolicyUpdateError;
@@ -414,6 +414,7 @@ pub(crate) struct CodexSpawnArgs {
pub(crate) analytics_events_client: Option<AnalyticsEventsClient>,
pub(crate) thread_store: Arc<dyn ThreadStore>,
pub(crate) attestation_provider: Option<Arc<dyn AttestationProvider>>,
pub(crate) state_db_access: StateDbAccess,
}
pub(crate) const INITIAL_SUBMIT_ID: &str = "";
@@ -474,6 +475,7 @@ impl Codex {
analytics_events_client,
thread_store,
attestation_provider,
state_db_access,
} = args;
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
let (tx_event, rx_event) = async_channel::unbounded();
@@ -563,12 +565,8 @@ impl Codex {
Some(thread_id) => {
let state_db_ctx = if config.ephemeral {
None
} else if let Some(local_store) =
thread_store.as_any().downcast_ref::<LocalThreadStore>()
{
local_store.state_db().await
} else {
None
state_db_access.state_db()
};
state_db::get_dynamic_tools(state_db_ctx.as_deref(), thread_id, "codex_spawn")
.await
@@ -660,6 +658,7 @@ impl Codex {
thread_store,
parent_rollout_thread_trace,
attestation_provider,
state_db_access,
)
.await
.map_err(|e| {
@@ -1049,7 +1048,7 @@ impl Session {
}
pub(crate) fn state_db(&self) -> Option<state_db::StateDbHandle> {
self.services.state_db.clone()
self.services.state_db_access.state_db()
}
pub(crate) fn live_thread_for_persistence(
@@ -1328,7 +1327,7 @@ impl Session {
self.services.user_shell.as_ref().clone(),
self.services.shell_snapshot_tx.clone(),
self.services.session_telemetry.clone(),
self.services.state_db.clone(),
self.services.state_db_access.clone(),
);
}
+6 -8
View File
@@ -1,4 +1,5 @@
use super::*;
use crate::StateDbAccess;
use crate::goals::GoalRuntimeState;
use codex_protocol::SessionId;
use codex_protocol::config_types::ServiceTier;
@@ -371,6 +372,7 @@ impl Session {
thread_store: Arc<dyn ThreadStore>,
parent_rollout_thread_trace: ThreadTraceContext,
attestation_provider: Option<Arc<dyn AttestationProvider>>,
state_db_access: StateDbAccess,
) -> anyhow::Result<Arc<Self>> {
debug!(
"Configuring session: model={}; provider={:?}",
@@ -471,12 +473,8 @@ impl Session {
let state_db_fut = async {
if config.ephemeral {
None
} else if let Some(local_store) =
thread_store.as_any().downcast_ref::<LocalThreadStore>()
{
local_store.state_db().await
} else {
None
state_db_access.state_db()
}
}
.instrument(info_span!(
@@ -507,7 +505,7 @@ impl Session {
));
// Join all independent futures.
let (thread_persistence_result, state_db_ctx, (auth, mcp_servers, auth_statuses)) =
let (thread_persistence_result, _state_db_ctx, (auth, mcp_servers, auth_statuses)) =
tokio::join!(thread_persistence_fut, state_db_fut, auth_and_mcp_fut);
let mut live_thread_init =
@@ -709,7 +707,7 @@ impl Session {
session_configuration.cwd.clone(),
&mut default_shell,
session_telemetry.clone(),
state_db_ctx.clone(),
state_db_access.clone(),
)
}
} else {
@@ -850,7 +848,7 @@ impl Session {
agent_control,
network_proxy,
network_approval: Arc::clone(&network_approval),
state_db: state_db_ctx.clone(),
state_db_access: state_db_access.clone(),
live_thread: live_thread_init.as_ref().cloned(),
thread_store: Arc::clone(&thread_store),
attestation_provider: attestation_provider.clone(),
+29 -16
View File
@@ -3731,10 +3731,11 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
/*analytics_events_client*/ None,
Arc::new(codex_thread_store::LocalThreadStore::new(
codex_thread_store::LocalThreadStoreConfig::from_config(config.as_ref()),
/*state_db*/ None,
crate::StateDbAccess::none(),
)),
codex_rollout_trace::ThreadTraceContext::disabled(),
/*attestation_provider*/ None,
crate::StateDbAccess::none(),
)
.await;
@@ -3877,11 +3878,11 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
agent_control,
network_proxy: None,
network_approval: Arc::clone(&network_approval),
state_db: None,
state_db_access: crate::StateDbAccess::none(),
live_thread: None,
thread_store: Arc::new(codex_thread_store::LocalThreadStore::new(
codex_thread_store::LocalThreadStoreConfig::from_config(config.as_ref()),
/*state_db*/ None,
crate::StateDbAccess::none(),
)),
attestation_provider: None,
model_client: ModelClient::new(
@@ -4070,10 +4071,11 @@ async fn make_session_with_config_and_rx(
/*analytics_events_client*/ None,
Arc::new(codex_thread_store::LocalThreadStore::new(
codex_thread_store::LocalThreadStoreConfig::from_config(config.as_ref()),
/*state_db*/ None,
crate::StateDbAccess::none(),
)),
codex_rollout_trace::ThreadTraceContext::disabled(),
/*attestation_provider*/ None,
crate::StateDbAccess::none(),
)
.await?;
@@ -4153,6 +4155,16 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
/*bundled_skills_enabled*/ true,
));
let state_db_access = crate::StateDbAccess::new(Some(
codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize"),
));
let session = Session::new(
session_configuration,
Arc::clone(&config),
@@ -4173,17 +4185,11 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
/*analytics_events_client*/ None,
Arc::new(codex_thread_store::LocalThreadStore::new(
codex_thread_store::LocalThreadStoreConfig::from_config(config.as_ref()),
Some(
codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.model_provider_id.clone(),
)
.await
.expect("state db should initialize"),
),
state_db_access.clone(),
)),
codex_rollout_trace::ThreadTraceContext::disabled(),
/*attestation_provider*/ None,
state_db_access,
)
.await?;
@@ -5463,6 +5469,7 @@ where
codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("goal tests should initialize sqlite state db"),
@@ -5470,6 +5477,7 @@ where
} else {
None
};
let state_db_access = crate::StateDbAccess::new(state_db.clone());
let config = Arc::new(config);
let thread_id = ThreadId::default();
let auth_manager = AuthManager::from_auth_for_testing(auth);
@@ -5596,11 +5604,11 @@ where
agent_control,
network_proxy: None,
network_approval: Arc::clone(&network_approval),
state_db: state_db.clone(),
state_db_access: state_db_access.clone(),
live_thread: None,
thread_store: Arc::new(codex_thread_store::LocalThreadStore::new(
codex_thread_store::LocalThreadStoreConfig::from_config(config.as_ref()),
state_db,
state_db_access,
)),
attestation_provider: None,
model_client: ModelClient::new(
@@ -7846,8 +7854,12 @@ async fn goal_test_state_db(sess: &Session) -> anyhow::Result<crate::StateDbHand
return Ok(state_db);
}
let config = sess.get_config().await;
codex_state::StateRuntime::init(config.sqlite_home.clone(), config.model_provider_id.clone())
.await
codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.model_provider_id.clone(),
/*metrics*/ None,
)
.await
}
#[tokio::test]
@@ -8161,6 +8173,7 @@ async fn completed_goal_accounts_current_turn_tokens_before_tool_response() -> a
let state_db = codex_state::StateRuntime::init(
test.config.sqlite_home.clone(),
test.config.model_provider_id.clone(),
/*metrics*/ None,
)
.await?;
let persisted_goal = state_db
@@ -731,7 +731,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
let skills_watcher = Arc::new(SkillsWatcher::noop());
let thread_store = Arc::new(codex_thread_store::LocalThreadStore::new(
codex_thread_store::LocalThreadStoreConfig::from_config(&config),
/*state_db*/ None,
crate::StateDbAccess::none(),
));
let CodexSpawnOk { codex, .. } = Codex::spawn(CodexSpawnArgs {
@@ -764,6 +764,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
analytics_events_client: None,
thread_store,
attestation_provider: None,
state_db_access: crate::StateDbAccess::none(),
})
.await
.expect("spawn guardian subagent");
+11 -11
View File
@@ -5,7 +5,7 @@ use std::sync::Arc;
use std::time::Duration;
use std::time::SystemTime;
use crate::StateDbHandle;
use crate::StateDbAccess;
use crate::rollout::list::find_thread_path_by_id_str;
use crate::shell::Shell;
use crate::shell::ShellType;
@@ -42,7 +42,7 @@ impl ShellSnapshot {
session_cwd: AbsolutePathBuf,
shell: &mut Shell,
session_telemetry: SessionTelemetry,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
) -> watch::Sender<Option<Arc<ShellSnapshot>>> {
let (shell_snapshot_tx, shell_snapshot_rx) = watch::channel(None);
shell.shell_snapshot = shell_snapshot_rx;
@@ -54,7 +54,7 @@ impl ShellSnapshot {
shell.clone(),
shell_snapshot_tx.clone(),
session_telemetry,
state_db,
state_db_access,
);
shell_snapshot_tx
@@ -67,7 +67,7 @@ impl ShellSnapshot {
shell: Shell,
shell_snapshot_tx: watch::Sender<Option<Arc<ShellSnapshot>>>,
session_telemetry: SessionTelemetry,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
) {
Self::spawn_snapshot_task(
codex_home,
@@ -76,7 +76,7 @@ impl ShellSnapshot {
shell,
shell_snapshot_tx,
session_telemetry,
state_db,
state_db_access,
);
}
@@ -87,7 +87,7 @@ impl ShellSnapshot {
snapshot_shell: Shell,
shell_snapshot_tx: watch::Sender<Option<Arc<ShellSnapshot>>>,
session_telemetry: SessionTelemetry,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
) {
let snapshot_span = info_span!("shell_snapshot", thread_id = %session_id);
tokio::spawn(
@@ -98,7 +98,7 @@ impl ShellSnapshot {
session_id,
&session_cwd,
&snapshot_shell,
state_db,
state_db_access,
)
.await
.map(Arc::new);
@@ -121,7 +121,7 @@ impl ShellSnapshot {
session_id: ThreadId,
session_cwd: &AbsolutePathBuf,
shell: &Shell,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
) -> std::result::Result<Self, &'static str> {
// File to store the snapshot
let extension = match shell.shell_type {
@@ -144,7 +144,7 @@ impl ShellSnapshot {
let cleanup_session_id = session_id;
tokio::spawn(async move {
if let Err(err) =
cleanup_stale_snapshots(&codex_home, cleanup_session_id, state_db).await
cleanup_stale_snapshots(&codex_home, cleanup_session_id, state_db_access).await
{
tracing::warn!("Failed to clean up shell snapshots: {err:?}");
}
@@ -500,7 +500,7 @@ $envVars | ForEach-Object {
pub async fn cleanup_stale_snapshots(
codex_home: &AbsolutePathBuf,
active_session_id: ThreadId,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
) -> Result<()> {
let snapshot_dir = codex_home.join(SNAPSHOT_DIR);
@@ -531,7 +531,7 @@ pub async fn cleanup_stale_snapshots(
}
let rollout_path =
find_thread_path_by_id_str(codex_home, session_id, state_db.as_deref()).await?;
find_thread_path_by_id_str(codex_home, session_id, &state_db_access).await?;
let Some(rollout_path) = rollout_path else {
remove_snapshot_file(&path).await;
continue;
+6 -6
View File
@@ -202,7 +202,7 @@ async fn try_new_creates_and_deletes_snapshot_file() -> Result<()> {
ThreadId::new(),
&dir.path().abs(),
&shell,
/*state_db*/ None,
crate::StateDbAccess::none(),
)
.await
.expect("snapshot should be created");
@@ -233,7 +233,7 @@ async fn try_new_uses_distinct_generation_paths() -> Result<()> {
session_id,
&dir.path().abs(),
&shell,
/*state_db*/ None,
crate::StateDbAccess::none(),
)
.await
.expect("initial snapshot should be created");
@@ -242,7 +242,7 @@ async fn try_new_uses_distinct_generation_paths() -> Result<()> {
session_id,
&dir.path().abs(),
&shell,
/*state_db*/ None,
crate::StateDbAccess::none(),
)
.await
.expect("refreshed snapshot should be created");
@@ -439,7 +439,7 @@ async fn cleanup_stale_snapshots_removes_orphans_and_keeps_live() -> Result<()>
fs::write(&orphan_snapshot, "orphan").await?;
fs::write(&invalid_snapshot, "invalid").await?;
cleanup_stale_snapshots(&codex_home, ThreadId::new(), /*state_db*/ None).await?;
cleanup_stale_snapshots(&codex_home, ThreadId::new(), crate::StateDbAccess::none()).await?;
assert_eq!(live_snapshot.exists(), true);
assert_eq!(orphan_snapshot.exists(), false);
@@ -462,7 +462,7 @@ async fn cleanup_stale_snapshots_removes_stale_rollouts() -> Result<()> {
set_file_mtime(&rollout_path, SNAPSHOT_RETENTION + Duration::from_secs(60))?;
cleanup_stale_snapshots(&codex_home, ThreadId::new(), /*state_db*/ None).await?;
cleanup_stale_snapshots(&codex_home, ThreadId::new(), crate::StateDbAccess::none()).await?;
assert_eq!(stale_snapshot.exists(), false);
Ok(())
@@ -483,7 +483,7 @@ async fn cleanup_stale_snapshots_skips_active_session() -> Result<()> {
set_file_mtime(&rollout_path, SNAPSHOT_RETENTION + Duration::from_secs(60))?;
cleanup_stale_snapshots(&codex_home, active_session, /*state_db*/ None).await?;
cleanup_stale_snapshots(&codex_home, active_session, crate::StateDbAccess::none()).await?;
assert_eq!(active_snapshot.exists(), true);
Ok(())
+2 -2
View File
@@ -24,7 +24,7 @@ use codex_login::AuthManager;
use codex_mcp::McpConnectionManager;
use codex_models_manager::manager::SharedModelsManager;
use codex_otel::SessionTelemetry;
use codex_rollout::state_db::StateDbHandle;
use codex_rollout::StateDbAccess;
use codex_rollout_trace::ThreadTraceContext;
use codex_thread_store::LiveThread;
use codex_thread_store::ThreadStore;
@@ -64,7 +64,7 @@ pub(crate) struct SessionServices {
pub(crate) agent_control: AgentControl,
pub(crate) network_proxy: Option<StartedNetworkProxy>,
pub(crate) network_approval: Arc<NetworkApprovalService>,
pub(crate) state_db: Option<StateDbHandle>,
pub(crate) state_db_access: StateDbAccess,
pub(crate) live_thread: Option<LiveThread>,
pub(crate) thread_store: Arc<dyn ThreadStore>,
pub(crate) attestation_provider: Option<Arc<dyn AttestationProvider>>,
+7 -2
View File
@@ -1,8 +1,13 @@
use codex_rollout::state_db as rollout_state_db;
pub use codex_rollout::state_db::StateDbAccess;
pub use codex_rollout::state_db::StateDbHandle;
pub use codex_state::DbMetricsRecorderHandle;
use crate::config::Config;
pub async fn init_state_db(config: &Config) -> Option<StateDbHandle> {
rollout_state_db::init(config).await
pub async fn init_state_db(
config: &Config,
metrics: Option<codex_state::DbMetricsRecorderHandle>,
) -> Option<StateDbHandle> {
rollout_state_db::init(config, metrics).await
}
+3 -3
View File
@@ -139,7 +139,7 @@ pub(crate) async fn record_completed_response_item(
}
mark_thread_memory_mode_polluted_if_external_context(sess, turn_context, item).await;
let has_memory_citation = record_stage1_output_usage_and_detect_memory_citation(
sess.services.state_db.as_ref(),
sess.services.state_db_access.as_deref(),
item,
)
.await;
@@ -169,7 +169,7 @@ pub(crate) async fn mark_thread_memory_mode_polluted_if_external_context(
return;
}
state_db::mark_thread_memory_mode_polluted(
sess.services.state_db.as_deref(),
sess.services.state_db_access.as_deref(),
sess.conversation_id,
"record_completed_response_item",
)
@@ -177,7 +177,7 @@ pub(crate) async fn mark_thread_memory_mode_polluted_if_external_context(
}
async fn record_stage1_output_usage_and_detect_memory_citation(
state_db_ctx: Option<&state_db::StateDbHandle>,
state_db_ctx: Option<&codex_state::StateRuntime>,
item: &ResponseItem,
) -> bool {
let Some(raw_text) = raw_assistant_output_text_from_item(item) else {
+11 -8
View File
@@ -51,6 +51,7 @@ use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::protocol::TurnAbortedEvent;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::W3cTraceContext;
use codex_rollout::StateDbAccess;
use codex_rollout::state_db::StateDbHandle;
use codex_state::DirectionalThreadSpawnEdgeStatus;
use codex_thread_store::InMemoryThreadStore;
@@ -253,7 +254,7 @@ pub(crate) struct ThreadManagerState {
session_source: SessionSource,
installation_id: String,
analytics_events_client: Option<AnalyticsEventsClient>,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
// Captures submitted ops for testing purpose when test mode is enabled.
ops_log: Option<SharedCapturedOps>,
}
@@ -271,12 +272,12 @@ pub fn build_models_manager(
pub fn thread_store_from_config(
config: &Config,
state_db: Option<StateDbHandle>,
state_db_ctx: StateDbAccess,
) -> Arc<dyn ThreadStore> {
match &config.experimental_thread_store {
ThreadStoreConfig::Local => Arc::new(LocalThreadStore::new(
LocalThreadStoreConfig::from_config(config),
state_db,
state_db_ctx,
)),
ThreadStoreConfig::InMemory { id } => InMemoryThreadStore::for_id(id),
}
@@ -291,7 +292,7 @@ impl ThreadManager {
environment_manager: Arc<EnvironmentManager>,
analytics_events_client: Option<AnalyticsEventsClient>,
thread_store: Arc<dyn ThreadStore>,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
installation_id: String,
attestation_provider: Option<Arc<dyn AttestationProvider>>,
) -> Self {
@@ -325,7 +326,7 @@ impl ThreadManager {
session_source,
installation_id,
analytics_events_client,
state_db,
state_db_access,
ops_log: should_use_test_thread_manager_behavior()
.then(|| Arc::new(std::sync::Mutex::new(Vec::new()))),
}),
@@ -402,13 +403,14 @@ impl ThreadManager {
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 state_db_access = StateDbAccess::new(state_db.clone());
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(),
},
state_db.clone(),
state_db_access.clone(),
));
Self {
state: Arc::new(ThreadManagerState {
@@ -427,7 +429,7 @@ impl ThreadManager {
session_source: SessionSource::Exec,
installation_id,
analytics_events_client: None,
state_db,
state_db_access,
ops_log: should_use_test_thread_manager_behavior()
.then(|| Arc::new(std::sync::Mutex::new(Vec::new()))),
}),
@@ -889,7 +891,7 @@ impl ThreadManager {
impl ThreadManagerState {
pub(crate) fn state_db(&self) -> Option<StateDbHandle> {
self.state_db.clone()
self.state_db_access.state_db()
}
pub(crate) async fn list_thread_ids(&self) -> Vec<ThreadId> {
@@ -1210,6 +1212,7 @@ impl ThreadManagerState {
analytics_events_client: self.analytics_events_client.clone(),
thread_store: Arc::clone(&self.thread_store),
attestation_provider: self.attestation_provider.clone(),
state_db_access: self.state_db_access.clone(),
})
.await?;
let new_thread = self
+40 -29
View File
@@ -486,14 +486,15 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let state_db_access = StateDbAccess::none();
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, /*state_db*/ None),
/*state_db*/ None,
thread_store_from_config(&config, state_db_access.clone()),
state_db_access,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
);
@@ -601,8 +602,9 @@ async fn explicit_installation_id_skips_codex_home_file() {
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let installation_id = uuid::Uuid::new_v4().to_string();
let state_db = init_state_db(&config).await;
let thread_store = thread_store_from_config(&config, state_db.clone());
let state_db = init_state_db(&config, /*metrics*/ None).await;
let state_db_access = StateDbAccess::new(state_db.clone());
let thread_store = thread_store_from_config(&config, state_db_access.clone());
let manager = ThreadManager::new(
&config,
auth_manager,
@@ -610,7 +612,7 @@ async fn explicit_installation_id_skips_codex_home_file() {
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
thread_store,
state_db.clone(),
state_db_access,
installation_id.clone(),
/*attestation_provider*/ None,
);
@@ -641,14 +643,15 @@ async fn resume_active_thread_from_rollout_returns_running_thread() {
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let state_db_access = StateDbAccess::none();
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, /*state_db*/ None),
/*state_db*/ None,
thread_store_from_config(&config, state_db_access.clone()),
state_db_access,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
);
@@ -697,14 +700,15 @@ async fn resume_stopped_thread_from_rollout_spawns_new_thread() {
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let state_db_access = StateDbAccess::none();
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, /*state_db*/ None),
/*state_db*/ None,
thread_store_from_config(&config, state_db_access.clone()),
state_db_access,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
);
@@ -758,8 +762,9 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() {
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let state_db = init_state_db(&config).await;
let thread_store = thread_store_from_config(&config, state_db.clone());
let state_db = init_state_db(&config, /*metrics*/ None).await;
let state_db_access = StateDbAccess::new(state_db.clone());
let thread_store = thread_store_from_config(&config, state_db_access.clone());
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
@@ -767,7 +772,7 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() {
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
thread_store,
state_db.clone(),
state_db_access,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
);
@@ -843,8 +848,9 @@ async fn rollout_path_resume_and_fork_read_history_through_thread_store() {
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let state_db = init_state_db(&config).await;
let thread_store = thread_store_from_config(&config, state_db.clone());
let state_db = init_state_db(&config, /*metrics*/ None).await;
let state_db_access = StateDbAccess::new(state_db.clone());
let thread_store = thread_store_from_config(&config, state_db_access.clone());
let in_memory_store = thread_store
.as_any()
.downcast_ref::<InMemoryThreadStore>()
@@ -856,7 +862,7 @@ async fn rollout_path_resume_and_fork_read_history_through_thread_store() {
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
thread_store.clone(),
state_db,
state_db_access,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
);
@@ -951,14 +957,15 @@ async fn new_uses_active_provider_for_model_refresh() {
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let state_db_access = StateDbAccess::none();
let manager = ThreadManager::new(
&config,
auth_manager,
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
thread_store_from_config(&config, /*state_db*/ None),
/*state_db*/ None,
thread_store_from_config(&config, state_db_access.clone()),
state_db_access,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
);
@@ -1165,15 +1172,16 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let state_db = init_state_db(&config).await;
let state_db = init_state_db(&config, /*metrics*/ None).await;
let state_db_access = StateDbAccess::new(state_db.clone());
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, state_db.clone()),
state_db.clone(),
thread_store_from_config(&config, state_db_access.clone()),
state_db_access,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
);
@@ -1272,15 +1280,16 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let state_db = init_state_db(&config).await;
let state_db = init_state_db(&config, /*metrics*/ None).await;
let state_db_access = StateDbAccess::new(state_db.clone());
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, state_db.clone()),
state_db.clone(),
thread_store_from_config(&config, state_db_access.clone()),
state_db_access,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
);
@@ -1368,15 +1377,16 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let state_db = init_state_db(&config).await;
let state_db = init_state_db(&config, /*metrics*/ None).await;
let state_db_access = StateDbAccess::new(state_db.clone());
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, state_db.clone()),
state_db.clone(),
thread_store_from_config(&config, state_db_access.clone()),
state_db_access,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
);
@@ -1510,15 +1520,16 @@ async fn resumed_thread_keeps_paused_goal_paused() -> anyhow::Result<()> {
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let state_db = init_state_db(&config).await;
let state_db = init_state_db(&config, /*metrics*/ None).await;
let state_db_access = StateDbAccess::new(state_db.clone());
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, state_db.clone()),
state_db.clone(),
thread_store_from_config(&config, state_db_access.clone()),
state_db_access,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
);
@@ -3148,8 +3148,33 @@ async fn close_agent_submits_shutdown_and_returns_previous_status() {
assert_eq!(status_after, AgentStatus::NotFound);
}
#[tokio::test]
async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtrees_closed() {
#[test]
fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtrees_closed() {
const TEST_STACK_SIZE_BYTES: usize = 8 * 1024 * 1024;
let handle = std::thread::Builder::new()
.name(
"tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtrees_closed"
.to_string(),
)
.stack_size(TEST_STACK_SIZE_BYTES)
.spawn(|| {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(Box::pin(
tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtrees_closed_impl(),
));
})
.expect("test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtrees_closed_impl() {
let (_session, turn) = make_session_and_context().await;
let mut config = turn.config.as_ref().clone();
config.agent_max_depth = 3;
@@ -3157,15 +3182,16 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
.features
.enable(Feature::Sqlite)
.expect("test config should allow sqlite");
let state_db = init_state_db(&config).await;
let state_db = init_state_db(&config, /*metrics*/ None).await;
let state_db_access = crate::StateDbAccess::new(state_db.clone());
let manager = ThreadManager::new(
&config,
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy")),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
thread_store_from_config(&config, state_db.clone()),
state_db.clone(),
thread_store_from_config(&config, state_db_access.clone()),
state_db_access,
"11111111-1111-4111-8111-111111111111".to_string(),
/*attestation_provider*/ None,
);
+8 -3
View File
@@ -66,6 +66,7 @@ type WorkspaceSetup = dyn FnOnce(AbsolutePathBuf, Arc<dyn ExecutorFileSystem>) -
+ Send;
const TEST_MODEL_WITH_EXPERIMENTAL_TOOLS: &str = "test-gpt-5.1-codex";
const REMOTE_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_TEST_REMOTE_EXEC_SERVER_URL";
const OTEL_SERVICE_NAME: &str = "codex_exec";
static REMOTE_TEST_INSTANCE_COUNTER: AtomicU64 = AtomicU64::new(0);
const SUBMIT_TURN_COMPLETE_TIMEOUT: Duration = Duration::from_secs(30);
@@ -423,8 +424,12 @@ impl TestCodexBuilder {
environment_manager: Arc<codex_exec_server::EnvironmentManager>,
) -> anyhow::Result<TestCodex> {
let auth = self.auth.clone();
let state_db = codex_core::init_state_db(&config).await;
let thread_store = thread_store_from_config(&config, state_db.clone());
let state_db_metrics =
codex_core::otel_init::sqlite_metrics_recorder_from_global(OTEL_SERVICE_NAME);
let state_db = codex_core::init_state_db(&config, state_db_metrics.clone()).await;
let state_db_access = codex_core::StateDbAccess::new(state_db.clone())
.with_standalone_metrics(state_db_metrics);
let thread_store = thread_store_from_config(&config, state_db_access.clone());
let installation_id = resolve_installation_id(&config.codex_home).await?;
let thread_manager = ThreadManager::new(
&config,
@@ -433,7 +438,7 @@ impl TestCodexBuilder {
Arc::clone(&environment_manager),
/*analytics_events_client*/ None,
thread_store,
state_db.clone(),
state_db_access,
installation_id,
/*attestation_provider*/ None,
);
+2 -2
View File
@@ -1138,8 +1138,8 @@ 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, /*state_db*/ None),
/*state_db*/ None,
thread_store_from_config(&config, codex_core::StateDbAccess::none()),
codex_core::StateDbAccess::none(),
installation_id,
/*attestation_provider*/ None,
);
@@ -1,6 +1,7 @@
use codex_config::config_toml::ConfigToml;
use codex_core::ARCHIVED_SESSIONS_SUBDIR;
use codex_core::SESSIONS_SUBDIR;
use codex_core::StateDbAccess;
use codex_core::personality_migration::PERSONALITY_MIGRATION_FILENAME;
use codex_core::personality_migration::PersonalityMigrationStatus;
use codex_core::personality_migration::maybe_migrate_personality;
@@ -144,7 +145,8 @@ async fn migration_marker_exists_no_sessions_no_change() -> io::Result<()> {
tokio::fs::write(&marker_path, "v1\n").await?;
let status =
maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?;
maybe_migrate_personality(temp.path(), &ConfigToml::default(), StateDbAccess::none())
.await?;
assert_eq!(status, PersonalityMigrationStatus::SkippedMarker);
assert_eq!(
@@ -159,7 +161,8 @@ async fn no_marker_no_sessions_no_change() -> io::Result<()> {
let temp = TempDir::new()?;
let status =
maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?;
maybe_migrate_personality(temp.path(), &ConfigToml::default(), StateDbAccess::none())
.await?;
assert_eq!(status, PersonalityMigrationStatus::SkippedNoSessions);
assert_eq!(
@@ -179,7 +182,8 @@ async fn no_marker_sessions_sets_personality() -> io::Result<()> {
write_session_with_user_event(temp.path()).await?;
let status =
maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?;
maybe_migrate_personality(temp.path(), &ConfigToml::default(), StateDbAccess::none())
.await?;
assert_eq!(status, PersonalityMigrationStatus::Applied);
assert_eq!(
@@ -199,7 +203,8 @@ async fn no_marker_sessions_preserves_existing_config_fields() -> io::Result<()>
tokio::fs::write(temp.path().join("config.toml"), "model = \"gpt-5.4\"\n").await?;
let config_toml = read_config_toml(temp.path()).await?;
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
let status =
maybe_migrate_personality(temp.path(), &config_toml, StateDbAccess::none()).await?;
assert_eq!(status, PersonalityMigrationStatus::Applied);
let persisted = read_config_toml(temp.path()).await?;
@@ -214,7 +219,8 @@ async fn no_marker_meta_only_rollout_is_treated_as_no_sessions() -> io::Result<(
write_session_with_meta_only(temp.path()).await?;
let status =
maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?;
maybe_migrate_personality(temp.path(), &ConfigToml::default(), StateDbAccess::none())
.await?;
assert_eq!(status, PersonalityMigrationStatus::SkippedNoSessions);
assert_eq!(
@@ -234,7 +240,8 @@ async fn no_marker_explicit_global_personality_skips_migration() -> io::Result<(
write_session_with_user_event(temp.path()).await?;
let config_toml = parse_config_toml("personality = \"friendly\"\n")?;
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
let status =
maybe_migrate_personality(temp.path(), &config_toml, StateDbAccess::none()).await?;
assert_eq!(
status,
@@ -264,7 +271,8 @@ personality = "friendly"
"#,
)?;
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
let status =
maybe_migrate_personality(temp.path(), &config_toml, StateDbAccess::none()).await?;
assert_eq!(
status,
@@ -287,7 +295,8 @@ async fn marker_short_circuits_invalid_profile_resolution() -> io::Result<()> {
tokio::fs::write(temp.path().join(PERSONALITY_MIGRATION_FILENAME), "v1\n").await?;
let config_toml = parse_config_toml("profile = \"missing\"\n")?;
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
let status =
maybe_migrate_personality(temp.path(), &config_toml, StateDbAccess::none()).await?;
assert_eq!(status, PersonalityMigrationStatus::SkippedMarker);
Ok(())
@@ -298,7 +307,7 @@ async fn invalid_selected_profile_returns_error_and_does_not_write_marker() -> i
let temp = TempDir::new()?;
let config_toml = parse_config_toml("profile = \"missing\"\n")?;
let err = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None)
let err = maybe_migrate_personality(temp.path(), &config_toml, StateDbAccess::none())
.await
.expect_err("missing profile should fail");
@@ -316,9 +325,11 @@ async fn applied_migration_is_idempotent_on_second_run() -> io::Result<()> {
write_session_with_user_event(temp.path()).await?;
let first_status =
maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?;
maybe_migrate_personality(temp.path(), &ConfigToml::default(), StateDbAccess::none())
.await?;
let second_status =
maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?;
maybe_migrate_personality(temp.path(), &ConfigToml::default(), StateDbAccess::none())
.await?;
assert_eq!(first_status, PersonalityMigrationStatus::Applied);
assert_eq!(second_status, PersonalityMigrationStatus::SkippedMarker);
@@ -333,7 +344,8 @@ async fn no_marker_archived_sessions_sets_personality() -> io::Result<()> {
write_archived_session_with_user_event(temp.path()).await?;
let status =
maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?;
maybe_migrate_personality(temp.path(), &ConfigToml::default(), StateDbAccess::none())
.await?;
assert_eq!(status, PersonalityMigrationStatus::Applied);
assert_eq!(
@@ -1,4 +1,5 @@
use anyhow::Result;
use codex_core::StateDbAccess;
use codex_core::build_prompt_input;
use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigOverrides;
@@ -29,7 +30,7 @@ async fn build_prompt_input_includes_context_and_user_message() -> Result<()> {
text: "hello from debug prompt".to_string(),
text_elements: Vec::new(),
}],
/*state_db*/ None,
StateDbAccess::none(),
)
.await?;
+45 -23
View File
@@ -66,9 +66,13 @@ async fn upsert_thread_metadata(
thread_id: ThreadId,
rollout_path: PathBuf,
) -> StateDbHandle {
let runtime = StateRuntime::init(codex_home.to_path_buf(), "test-provider".to_string())
.await
.unwrap();
let runtime = StateRuntime::init(
codex_home.to_path_buf(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.unwrap();
runtime
.mark_backfill_complete(/*last_watermark*/ None)
.await
@@ -91,10 +95,13 @@ async fn find_locates_rollout_file_by_id() {
let id = Uuid::new_v4();
let expected = write_minimal_rollout_with_id(home.path(), id);
let found =
find_thread_path_by_id_str(home.path(), &id.to_string(), /*state_db_ctx*/ None)
.await
.unwrap();
let found = find_thread_path_by_id_str(
home.path(),
&id.to_string(),
&codex_core::StateDbAccess::none(),
)
.await
.unwrap();
assert_eq!(found.unwrap(), expected);
}
@@ -108,10 +115,13 @@ async fn find_handles_gitignore_covering_codex_home_directory() {
let id = Uuid::new_v4();
let expected = write_minimal_rollout_with_id(&codex_home, id);
let found =
find_thread_path_by_id_str(&codex_home, &id.to_string(), /*state_db_ctx*/ None)
.await
.unwrap();
let found = find_thread_path_by_id_str(
&codex_home,
&id.to_string(),
&codex_core::StateDbAccess::none(),
)
.await
.unwrap();
assert_eq!(found, Some(expected));
}
@@ -129,9 +139,13 @@ async fn find_prefers_sqlite_path_by_id() {
write_minimal_rollout_with_id(home.path(), id);
let state_db = upsert_thread_metadata(home.path(), thread_id, db_path.clone()).await;
let found = find_thread_path_by_id_str(home.path(), &id.to_string(), Some(&state_db))
.await
.unwrap();
let found = find_thread_path_by_id_str(
home.path(),
&id.to_string(),
&codex_core::StateDbAccess::new(Some(state_db.clone())),
)
.await
.unwrap();
assert_eq!(found, Some(db_path));
}
@@ -148,9 +162,13 @@ async fn find_falls_back_to_filesystem_when_sqlite_has_no_match() {
.join("sessions/2030/12/30/rollout-2030-12-30T00-00-00-unrelated.jsonl");
let state_db = upsert_thread_metadata(home.path(), unrelated_thread_id, unrelated_path).await;
let found = find_thread_path_by_id_str(home.path(), &id.to_string(), Some(&state_db))
.await
.unwrap();
let found = find_thread_path_by_id_str(
home.path(),
&id.to_string(),
&codex_core::StateDbAccess::new(Some(state_db.clone())),
)
.await
.unwrap();
assert_eq!(found, Some(expected));
}
@@ -162,10 +180,13 @@ async fn find_ignores_granular_gitignore_rules() {
let expected = write_minimal_rollout_with_id(home.path(), id);
std::fs::write(home.path().join("sessions/.gitignore"), "*.jsonl\n").unwrap();
let found =
find_thread_path_by_id_str(home.path(), &id.to_string(), /*state_db_ctx*/ None)
.await
.unwrap();
let found = find_thread_path_by_id_str(
home.path(),
&id.to_string(),
&codex_core::StateDbAccess::none(),
)
.await
.unwrap();
assert_eq!(found, Some(expected));
}
@@ -212,7 +233,8 @@ async fn find_locates_rollout_file_written_by_recorder() -> std::io::Result<()>
)?;
let found =
find_thread_meta_by_name_str(home.path(), thread_name, /*state_db_ctx*/ None).await?;
find_thread_meta_by_name_str(home.path(), thread_name, &codex_core::StateDbAccess::none())
.await?;
let (path, session_meta) = found.expect("expected rollout path to be found");
assert_eq!(session_meta.meta.id, thread_id);
@@ -232,7 +254,7 @@ async fn find_archived_locates_rollout_file_by_id() {
let found = find_archived_thread_path_by_id_str(
home.path(),
&id.to_string(),
/*state_db_ctx*/ None,
&codex_core::StateDbAccess::none(),
)
.await
.unwrap();
+15 -5
View File
@@ -56,6 +56,7 @@ use codex_cloud_requirements::cloud_requirements_loader_for_storage;
use codex_config::ConfigLoadError;
use codex_config::LoaderOverrides;
use codex_config::format_config_error_with_source;
use codex_core::StateDbAccess;
use codex_core::StateDbHandle;
use codex_core::check_execpolicy_for_warnings;
use codex_core::config::Config;
@@ -156,6 +157,7 @@ use crate::event_processor::EventProcessor;
const DEFAULT_ANALYTICS_ENABLED: bool = true;
const EXEC_DEFAULT_LOG_FILTER: &str = "error,opentelemetry_sdk=off,opentelemetry_otlp=off";
const OTEL_SERVICE_NAME: &str = "codex_exec";
enum InitialOperation {
UserTurn {
@@ -482,6 +484,9 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
let otel_logger_layer = otel.as_ref().and_then(|o| o.logger_layer());
let otel_tracing_layer = otel.as_ref().and_then(|o| o.tracing_layer());
codex_core::otel_init::record_process_start(otel.as_ref(), OTEL_SERVICE_NAME);
let state_db_metrics =
codex_core::otel_init::sqlite_metrics_recorder(otel.as_ref(), OTEL_SERVICE_NAME);
let _ = tracing_subscriber::registry()
.with(fmt_layer)
@@ -507,7 +512,9 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
arg0_paths.codex_self_exe.clone(),
arg0_paths.codex_linux_sandbox_exe.clone(),
)?;
let state_db = codex_core::init_state_db(&config).await;
let state_db = codex_core::init_state_db(&config, state_db_metrics.clone()).await;
let state_db_access =
StateDbAccess::new(state_db.clone()).with_standalone_metrics(state_db_metrics);
let environment_manager = if run_loader_overrides.ignore_user_config {
EnvironmentManager::from_env(local_runtime_paths).await?
} else {
@@ -521,7 +528,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
cloud_requirements: run_cloud_requirements,
feedback: CodexFeedback::new(),
log_db: None,
state_db: state_db.clone(),
state_db_access,
environment_manager: std::sync::Arc::new(environment_manager),
config_warnings,
session_source: SessionSource::Exec,
@@ -1398,9 +1405,12 @@ async fn resolve_resume_thread_id(
if let Some(thread) = resolved {
return Ok(Some(thread.id.to_string()));
}
if let Some((_, session_meta)) =
find_thread_meta_by_name_str(&config.codex_home, session_id, Some(state_db.as_ref()))
.await?
if let Some((_, session_meta)) = find_thread_meta_by_name_str(
&config.codex_home,
session_id,
&StateDbAccess::new(Some((*state_db).clone())),
)
.await?
&& (args.all || cwds_match(config.cwd.as_path(), &session_meta.meta.cwd))
{
return Ok(Some(session_meta.meta.id.to_string()));
+32 -16
View File
@@ -74,19 +74,6 @@ pub async fn run_main(
std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}"))
})?;
set_default_client_residency_requirement(config.enforce_residency.value());
let state_db = codex_core::init_state_db(&config).await;
let environment_manager = Arc::new(
EnvironmentManager::from_codex_home(
config.codex_home.clone(),
ExecServerRuntimePaths::from_optional_paths(
arg0_paths.codex_self_exe.clone(),
arg0_paths.codex_linux_sandbox_exe.clone(),
)?,
)
.await
.map_err(std::io::Error::other)?,
);
let otel = codex_core::otel_init::build_provider(
&config,
env!("CARGO_PKG_VERSION"),
@@ -99,6 +86,23 @@ pub async fn run_main(
format!("error loading otel config: {e}"),
)
})?;
codex_core::otel_init::record_process_start(otel.as_ref(), OTEL_SERVICE_NAME);
let state_db_metrics =
codex_core::otel_init::sqlite_metrics_recorder(otel.as_ref(), OTEL_SERVICE_NAME);
let state_db = codex_core::init_state_db(&config, state_db_metrics.clone()).await;
let state_db_access =
codex_core::StateDbAccess::new(state_db.clone()).with_standalone_metrics(state_db_metrics);
let environment_manager = Arc::new(
EnvironmentManager::from_codex_home(
config.codex_home.clone(),
ExecServerRuntimePaths::from_optional_paths(
arg0_paths.codex_self_exe.clone(),
arg0_paths.codex_linux_sandbox_exe.clone(),
)?,
)
.await
.map_err(std::io::Error::other)?,
);
let fmt_layer = tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
@@ -148,7 +152,7 @@ pub async fn run_main(
arg0_paths,
Arc::new(config),
environment_manager,
state_db,
state_db_access,
installation_id,
)
.await;
@@ -201,10 +205,15 @@ pub async fn run_main(
mod tests {
use super::*;
use codex_config::types::OtelExporterKind;
use codex_config::types::OtelHttpProtocol;
use codex_core::config::ConfigBuilder;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use tempfile::TempDir;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
#[test]
fn mcp_server_defaults_analytics_to_enabled() {
@@ -213,14 +222,21 @@ mod tests {
#[tokio::test]
async fn mcp_server_builds_otel_provider_with_logs_traces_and_metrics() -> anyhow::Result<()> {
let collector = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200))
.mount(&collector)
.await;
let codex_home = TempDir::new()?;
let mut config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await?;
let exporter = OtelExporterKind::OtlpGrpc {
endpoint: "http://localhost:4317".to_string(),
let exporter = OtelExporterKind::OtlpHttp {
endpoint: collector.uri(),
headers: HashMap::new(),
protocol: OtelHttpProtocol::Binary,
tls: None,
};
config.otel.exporter = exporter.clone();
+4 -4
View File
@@ -2,7 +2,7 @@ use std::collections::HashMap;
use std::sync::Arc;
use codex_arg0::Arg0DispatchPaths;
use codex_core::StateDbHandle;
use codex_core::StateDbAccess;
use codex_core::ThreadManager;
use codex_core::config::Config;
use codex_core::thread_store_from_config;
@@ -54,7 +54,7 @@ impl MessageProcessor {
arg0_paths: Arg0DispatchPaths,
config: Arc<Config>,
environment_manager: Arc<EnvironmentManager>,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
installation_id: String,
) -> Self {
let outgoing = Arc::new(outgoing);
@@ -69,8 +69,8 @@ impl MessageProcessor {
SessionSource::Mcp,
environment_manager,
/*analytics_events_client*/ None,
thread_store_from_config(config.as_ref(), state_db.clone()),
state_db.clone(),
thread_store_from_config(config.as_ref(), state_db_access.clone()),
state_db_access,
installation_id,
/*attestation_provider*/ None,
));
+6 -2
View File
@@ -308,8 +308,12 @@ async fn build_test_codex(
}
async fn init_state_db(home: &Arc<TempDir>) -> anyhow::Result<Arc<codex_state::StateRuntime>> {
let db =
codex_state::StateRuntime::init(home.path().to_path_buf(), "test-provider".into()).await?;
let db = codex_state::StateRuntime::init(
home.path().to_path_buf(),
"test-provider".into(),
/*metrics*/ None,
)
.await?;
db.mark_backfill_complete(/*last_watermark*/ None).await?;
Ok(db)
}
+8 -3
View File
@@ -41,6 +41,7 @@ use std::time::Duration;
use tracing::debug;
const ENV_ATTRIBUTE: &str = "env";
const ARCH_ATTRIBUTE: &str = "arch";
const METER_NAME: &str = "codex";
const DURATION_UNIT: &str = "ms";
const DURATION_DESCRIPTION: &str = "Duration in milliseconds.";
@@ -198,13 +199,13 @@ impl MetricsClient {
validate_tags(&default_tags)?;
let mut resource_attributes = Vec::with_capacity(4);
let mut resource_attributes = Vec::with_capacity(5);
resource_attributes.push(KeyValue::new(
semconv::attribute::SERVICE_VERSION,
service_version,
));
resource_attributes.push(KeyValue::new(ENV_ATTRIBUTE, environment));
resource_attributes.extend(os_resource_attributes());
resource_attributes.extend(platform_resource_attributes());
let resource = Resource::builder()
.with_service_name(service_name)
@@ -290,12 +291,13 @@ impl MetricsClient {
}
}
fn os_resource_attributes() -> Vec<KeyValue> {
fn platform_resource_attributes() -> Vec<KeyValue> {
let os_info = os_info::get();
let os_type_raw = os_info.os_type().to_string();
let os_type = sanitize_metric_tag_value(os_type_raw.as_str());
let os_version_raw = os_info.version().to_string();
let os_version = sanitize_metric_tag_value(os_version_raw.as_str());
let arch = sanitize_metric_tag_value(std::env::consts::ARCH);
let mut attributes = Vec::new();
if os_type != "unspecified" {
attributes.push(KeyValue::new("os", os_type));
@@ -303,6 +305,9 @@ fn os_resource_attributes() -> Vec<KeyValue> {
if os_version != "unspecified" {
attributes.push(KeyValue::new("os_version", os_version));
}
if arch != "unspecified" {
attributes.push(KeyValue::new(ARCH_ATTRIBUTE, arch));
}
attributes
}
+4
View File
@@ -2,6 +2,7 @@ mod client;
mod config;
mod error;
pub(crate) mod names;
mod process;
pub(crate) mod runtime_metrics;
pub(crate) mod tags;
pub(crate) mod timer;
@@ -13,9 +14,12 @@ pub use crate::metrics::config::MetricsConfig;
pub use crate::metrics::config::MetricsExporter;
pub use crate::metrics::error::MetricsError;
pub use crate::metrics::error::Result;
pub use crate::metrics::process::record_process_start_once;
pub use names::*;
use std::sync::OnceLock;
pub use tags::ORIGINATOR_TAG;
pub use tags::SessionMetricTagValues;
pub use tags::bounded_originator_tag_value;
static GLOBAL_METRICS: OnceLock<MetricsClient> = OnceLock::new();
static GLOBAL_STATSIG_METRICS_SETTINGS: OnceLock<StatsigMetricsSettings> = OnceLock::new();
+1
View File
@@ -1,6 +1,7 @@
pub const TOOL_CALL_COUNT_METRIC: &str = "codex.tool.call";
pub const TOOL_CALL_DURATION_METRIC: &str = "codex.tool.call.duration_ms";
pub const TOOL_CALL_UNIFIED_EXEC_METRIC: &str = "codex.tool.unified_exec";
pub const PROCESS_START_METRIC: &str = "codex.process.start";
pub const API_CALL_COUNT_METRIC: &str = "codex.api_request";
pub const API_CALL_DURATION_METRIC: &str = "codex.api_request.duration_ms";
pub const SSE_EVENT_COUNT_METRIC: &str = "codex.sse_event";
+27
View File
@@ -0,0 +1,27 @@
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use super::client::MetricsClient;
use super::error::Result;
use super::names::PROCESS_START_METRIC;
use super::tags::ORIGINATOR_TAG;
use super::tags::bounded_originator_tag_value;
static PROCESS_START_RECORDED: AtomicBool = AtomicBool::new(false);
/// Record the process start counter at most once for this process.
pub fn record_process_start_once(metrics: &MetricsClient, originator: &str) -> Result<bool> {
if PROCESS_START_RECORDED
.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
.is_err()
{
return Ok(false);
}
metrics.counter(
PROCESS_START_METRIC,
/*inc*/ 1,
&[(ORIGINATOR_TAG, bounded_originator_tag_value(originator))],
)?;
Ok(true)
}
+26
View File
@@ -1,6 +1,7 @@
use crate::metrics::Result;
use crate::metrics::validation::validate_tag_key;
use crate::metrics::validation::validate_tag_value;
use codex_utils_string::sanitize_metric_tag_value;
pub const APP_VERSION_TAG: &str = "app.version";
pub const AUTH_MODE_TAG: &str = "auth_mode";
@@ -9,6 +10,31 @@ pub const ORIGINATOR_TAG: &str = "originator";
pub const SERVICE_NAME_TAG: &str = "service_name";
pub const SESSION_SOURCE_TAG: &str = "session_source";
const OTHER_ORIGINATOR_TAG_VALUE: &str = "other";
const KNOWN_ORIGINATOR_TAG_VALUES: &[&str] = &[
"codex_desktop",
"codex-app-server",
"codex_mcp_server",
"codex_cli_rs",
"codex-tui",
"codex_vscode",
"none",
"codex_exec",
"codex-cli",
"codex_sdk_ts",
"codex-app-server-sdk",
];
/// Returns a known low-cardinality originator tag value, or `other`.
pub fn bounded_originator_tag_value(originator: &str) -> &'static str {
let sanitized = sanitize_metric_tag_value(originator);
KNOWN_ORIGINATOR_TAG_VALUES
.iter()
.copied()
.find(|known| *known == sanitized.as_str())
.unwrap_or(OTHER_ORIGINATOR_TAG_VALUE)
}
pub struct SessionMetricTagValues<'a> {
pub auth_mode: Option<&'a str>,
pub session_source: &'a str,
+2
View File
@@ -10,6 +10,7 @@ pub(crate) mod metadata;
pub(crate) mod policy;
pub(crate) mod recorder;
pub(crate) mod session_index;
pub(crate) mod sqlite_metrics;
pub mod state_db;
pub(crate) mod default_client {
@@ -62,6 +63,7 @@ pub use session_index::append_thread_name;
pub use session_index::find_thread_meta_by_name_str;
pub use session_index::find_thread_name_by_id;
pub use session_index::find_thread_names_by_ids;
pub use state_db::StateDbAccess;
pub use state_db::StateDbHandle;
#[cfg(test)]
+74 -35
View File
@@ -20,6 +20,7 @@ use super::ARCHIVED_SESSIONS_SUBDIR;
use super::SESSIONS_SUBDIR;
use crate::protocol::EventMsg;
use crate::state_db;
use crate::state_db::StateDbAccess;
use codex_file_search as file_search;
use codex_protocol::ThreadId;
use codex_protocol::protocol::RolloutItem;
@@ -1240,7 +1241,9 @@ async fn find_thread_path_by_id_str_in_subdir(
subdir: &str,
id_str: &str,
state_db_ctx: Option<&codex_state::StateRuntime>,
metrics: Option<&dyn codex_state::DbMetricsRecorder>,
) -> io::Result<Option<PathBuf>> {
let metrics = metrics.or_else(|| state_db_ctx.and_then(codex_state::StateRuntime::metrics));
// Validate UUID format early.
if Uuid::parse_str(id_str).is_err() {
return Ok(None);
@@ -1255,47 +1258,67 @@ async fn find_thread_path_by_id_str_in_subdir(
};
let thread_id = ThreadId::from_string(id_str).ok();
let mut unverified_db_path = None;
let mut fallback_reason = state_db_ctx.is_none().then_some("db_unavailable");
if let Some(state_db_ctx) = state_db_ctx
&& let Some(thread_id) = thread_id
&& let Some(db_path) = state_db::find_rollout_path_by_id(
Some(state_db_ctx),
thread_id,
archived_only,
"find_path_query",
)
.await
{
if tokio::fs::try_exists(&db_path).await.unwrap_or(false) {
match read_session_meta_line(&db_path).await {
Ok(meta_line) if meta_line.meta.id == thread_id => {
return Ok(Some(db_path));
}
Ok(meta_line) => {
match state_db_ctx
.find_rollout_path_by_id(thread_id, archived_only)
.await
{
Ok(Some(db_path)) => {
if tokio::fs::try_exists(&db_path).await.unwrap_or(false) {
match read_session_meta_line(&db_path).await {
Ok(meta_line) if meta_line.meta.id == thread_id => {
return Ok(Some(db_path));
}
Ok(meta_line) => {
tracing::error!(
"state db returned rollout path for thread {id_str} but file belongs to thread {}: {}",
meta_line.meta.id,
db_path.display()
);
tracing::warn!(
"state db discrepancy during find_thread_path_by_id_str_in_subdir: mismatched_db_path"
);
codex_state::record_db_fallback_metric(
metrics,
"find_thread_path",
"mismatch",
);
}
Err(err) => {
tracing::debug!(
"state db returned rollout path for thread {id_str} that could not be verified: {}: {err}",
db_path.display()
);
unverified_db_path = Some(db_path);
}
}
} else {
tracing::error!(
"state db returned rollout path for thread {id_str} but file belongs to thread {}: {}",
meta_line.meta.id,
"state db returned stale rollout path for thread {id_str}: {}",
db_path.display()
);
tracing::warn!(
"state db discrepancy during find_thread_path_by_id_str_in_subdir: mismatched_db_path"
"state db discrepancy during find_thread_path_by_id_str_in_subdir: stale_db_path"
);
}
Err(err) => {
tracing::debug!(
"state db returned rollout path for thread {id_str} that could not be verified: {}: {err}",
db_path.display()
codex_state::record_db_fallback_metric(
metrics,
"find_thread_path",
"stale_path",
);
unverified_db_path = Some(db_path);
}
}
} else {
tracing::error!(
"state db returned stale rollout path for thread {id_str}: {}",
db_path.display()
);
tracing::warn!(
"state db discrepancy during find_thread_path_by_id_str_in_subdir: stale_db_path"
);
Ok(None) => {
fallback_reason = Some("missing_row");
}
Err(err) => {
tracing::warn!(
"state db find_rollout_path_by_id failed during find_path_query: {err}"
);
fallback_reason = Some("db_error");
}
}
}
@@ -1323,6 +1346,9 @@ async fn find_thread_path_by_id_str_in_subdir(
tracing::warn!(
"state db discrepancy during find_thread_path_by_id_str_in_subdir: falling_back"
);
if let Some(reason) = fallback_reason {
codex_state::record_db_fallback_metric(metrics, "find_thread_path", reason);
}
state_db::read_repair_rollout_path(
state_db_ctx,
thread_id,
@@ -1341,19 +1367,32 @@ async fn find_thread_path_by_id_str_in_subdir(
pub async fn find_thread_path_by_id_str(
codex_home: &Path,
id_str: &str,
state_db_ctx: Option<&codex_state::StateRuntime>,
state_db_ctx: &StateDbAccess,
) -> io::Result<Option<PathBuf>> {
find_thread_path_by_id_str_in_subdir(codex_home, SESSIONS_SUBDIR, id_str, state_db_ctx).await
find_thread_path_by_id_str_in_subdir(
codex_home,
SESSIONS_SUBDIR,
id_str,
state_db_ctx.as_deref(),
state_db_ctx.metrics(),
)
.await
}
/// Locate an archived thread rollout file by its UUID string.
pub async fn find_archived_thread_path_by_id_str(
codex_home: &Path,
id_str: &str,
state_db_ctx: Option<&codex_state::StateRuntime>,
state_db_ctx: &StateDbAccess,
) -> io::Result<Option<PathBuf>> {
find_thread_path_by_id_str_in_subdir(codex_home, ARCHIVED_SESSIONS_SUBDIR, id_str, state_db_ctx)
.await
find_thread_path_by_id_str_in_subdir(
codex_home,
ARCHIVED_SESSIONS_SUBDIR,
id_str,
state_db_ctx.as_deref(),
state_db_ctx.metrics(),
)
.await
}
/// Extract the `YYYY/MM/DD` directory components from a rollout filename.
+21 -9
View File
@@ -187,9 +187,13 @@ async fn backfill_sessions_resumes_from_watermark_and_marks_complete() {
/*git*/ None,
);
let runtime = codex_state::StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = codex_state::StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let first_watermark = backfill_watermark_for_path(codex_home.as_path(), first_path.as_path());
runtime.mark_backfill_running().await.expect("mark running");
runtime
@@ -252,9 +256,13 @@ async fn backfill_sessions_preserves_existing_git_branch_and_fills_missing_git_f
}),
);
let runtime = codex_state::StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = codex_state::StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id = ThreadId::from_string(&thread_uuid.to_string()).expect("thread id");
let mut existing = extract_metadata_from_rollout(&rollout_path, "test-provider")
.await
@@ -298,9 +306,13 @@ async fn backfill_sessions_normalizes_cwd_before_upsert() {
/*git*/ None,
);
let runtime = codex_state::StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = codex_state::StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
backfill_sessions(runtime.as_ref(), codex_home.as_path(), "test-provider").await;
+42 -11
View File
@@ -53,6 +53,7 @@ use super::session_index::find_thread_names_by_ids;
use crate::config::RolloutConfigView;
use crate::default_client::originator;
use crate::state_db;
use crate::state_db::StateDbAccess;
use crate::state_db::StateDbHandle;
use codex_git_utils::collect_git_info;
use codex_protocol::protocol::EventMsg;
@@ -236,7 +237,7 @@ impl RolloutRecorder {
/// List threads (rollout files) under the provided Codex home directory.
#[allow(clippy::too_many_arguments)]
pub async fn list_threads(
state_db_ctx: Option<StateDbHandle>,
state_db_ctx: StateDbAccess,
config: &impl RolloutConfigView,
page_size: usize,
cursor: Option<&Cursor>,
@@ -268,7 +269,7 @@ impl RolloutRecorder {
#[allow(clippy::too_many_arguments)]
pub async fn list_threads_from_state_db(
state_db_ctx: Option<StateDbHandle>,
state_db_ctx: StateDbAccess,
config: &impl RolloutConfigView,
page_size: usize,
cursor: Option<&Cursor>,
@@ -301,7 +302,7 @@ impl RolloutRecorder {
/// List archived threads (rollout files) under the archived sessions directory.
#[allow(clippy::too_many_arguments)]
pub async fn list_archived_threads(
state_db_ctx: Option<StateDbHandle>,
state_db_ctx: StateDbAccess,
config: &impl RolloutConfigView,
page_size: usize,
cursor: Option<&Cursor>,
@@ -333,7 +334,7 @@ impl RolloutRecorder {
#[allow(clippy::too_many_arguments)]
pub async fn list_archived_threads_from_state_db(
state_db_ctx: Option<StateDbHandle>,
state_db_ctx: StateDbAccess,
config: &impl RolloutConfigView,
page_size: usize,
cursor: Option<&Cursor>,
@@ -365,7 +366,7 @@ impl RolloutRecorder {
#[allow(clippy::too_many_arguments)]
async fn list_threads_with_db_fallback(
state_db_ctx: Option<StateDbHandle>,
state_db_ctx: StateDbAccess,
config: &impl RolloutConfigView,
page_size: usize,
cursor: Option<&Cursor>,
@@ -447,9 +448,14 @@ impl RolloutRecorder {
}
};
if state_db_ctx.is_none() {
if !state_db_ctx.is_available() {
// Keep legacy behavior when SQLite is unavailable: return filesystem results
// at the requested page size.
codex_state::record_db_fallback_metric(
state_db_ctx.metrics(),
"list_threads",
"db_unavailable",
);
return Ok(page_from_filesystem_scan(
fs_page,
sort_direction,
@@ -558,6 +564,11 @@ impl RolloutRecorder {
)
.await;
}
codex_state::record_db_fallback_metric(
state_db_ctx.metrics(),
"list_threads",
"metadata_filter",
);
let page = page_from_filesystem_scan(fs_page, sort_direction, page_size, sort_key);
return Ok(fill_missing_thread_item_metadata_from_state_db(
state_db_ctx.as_deref(),
@@ -569,6 +580,11 @@ impl RolloutRecorder {
}
if listing_has_metadata_filters {
let page = page_from_filesystem_scan(fs_page, sort_direction, page_size, sort_key);
codex_state::record_db_fallback_metric(
state_db_ctx.metrics(),
"list_threads",
"db_error",
);
return Ok(fill_missing_thread_item_metadata_from_state_db(
state_db_ctx.as_deref(),
page,
@@ -578,6 +594,7 @@ impl RolloutRecorder {
// If SQLite listing still fails, return the filesystem page rather than failing the list.
tracing::error!("Falling back on rollout system");
tracing::warn!("state db discrepancy during list_threads_with_db_fallback: falling_back");
codex_state::record_db_fallback_metric(state_db_ctx.metrics(), "list_threads", "db_error");
Ok(page_from_filesystem_scan(
fs_page,
sort_direction,
@@ -589,7 +606,7 @@ impl RolloutRecorder {
/// Find the newest recorded thread path, optionally filtering to a matching cwd.
#[allow(clippy::too_many_arguments)]
pub async fn find_latest_thread_path(
state_db_ctx: Option<StateDbHandle>,
state_db_ctx: StateDbAccess,
config: &impl RolloutConfigView,
page_size: usize,
cursor: Option<&Cursor>,
@@ -601,10 +618,11 @@ impl RolloutRecorder {
) -> std::io::Result<Option<PathBuf>> {
let codex_home = config.codex_home();
let cwd_filter = filter_cwd.map(Path::to_path_buf);
if state_db_ctx.is_some() {
let mut fallback_reason = (!state_db_ctx.is_available()).then_some("db_unavailable");
if state_db_ctx.is_available() {
let mut db_cursor = cursor.cloned();
loop {
let Some(db_page) = state_db::list_threads_db(
let db_page = match state_db::list_threads_db(
state_db_ctx.as_deref(),
codex_home,
page_size,
@@ -618,8 +636,12 @@ impl RolloutRecorder {
/*search_term*/ None,
)
.await
else {
break;
{
Some(db_page) => db_page,
None => {
fallback_reason = Some("db_error");
break;
}
};
if let Some(path) =
select_resume_path_from_db_page(&db_page, filter_cwd, default_provider).await
@@ -628,11 +650,20 @@ impl RolloutRecorder {
}
db_cursor = db_page.next_anchor.map(Into::into);
if db_cursor.is_none() {
fallback_reason = Some("missing_row");
break;
}
}
}
if let Some(reason) = fallback_reason {
codex_state::record_db_fallback_metric(
state_db_ctx.metrics(),
"find_latest_thread_path",
reason,
);
}
let mut cursor = cursor.cloned();
loop {
let page = get_threads(
+140 -24
View File
@@ -18,11 +18,14 @@ use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::TurnContextItem;
use codex_protocol::protocol::UserMessageEvent;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use tempfile::TempDir;
use uuid::Uuid;
@@ -37,6 +40,45 @@ fn test_config(codex_home: &Path) -> RolloutConfig {
}
}
#[derive(Default)]
struct TestMetrics {
events: Mutex<Vec<MetricEvent>>,
}
#[derive(Debug, Eq, PartialEq)]
struct MetricEvent {
name: String,
tags: BTreeMap<String, String>,
}
impl TestMetrics {
fn has_fallback(&self, caller: &str, reason: &str) -> bool {
self.events
.lock()
.expect("metrics lock")
.iter()
.any(|event| {
event.name == codex_state::DB_FALLBACK_METRIC
&& event.tags.get("caller").map(String::as_str) == Some(caller)
&& event.tags.get("reason").map(String::as_str) == Some(reason)
})
}
}
impl codex_state::DbMetricsRecorder for TestMetrics {
fn counter(&self, name: &str, _inc: i64, tags: &[(&str, &str)]) {
self.events.lock().expect("metrics lock").push(MetricEvent {
name: name.to_string(),
tags: tags
.iter()
.map(|(key, value)| ((*key).to_string(), (*value).to_string()))
.collect(),
});
}
fn record_duration(&self, _name: &str, _duration: Duration, _tags: &[(&str, &str)]) {}
}
fn write_session_file(root: &Path, ts: &str, uuid: Uuid) -> std::io::Result<PathBuf> {
let day_dir = root.join("sessions/2025/01/03");
fs::create_dir_all(&day_dir)?;
@@ -124,7 +166,7 @@ async fn state_db_init_backfills_before_returning() -> anyhow::Result<()> {
.join("\n");
fs::write(&rollout_path, format!("{jsonl}\n"))?;
let runtime = crate::state_db::init(&test_config(home.path()))
let runtime = crate::state_db::init(&test_config(home.path()), /*metrics*/ None)
.await
.expect("state db should initialize");
@@ -535,9 +577,13 @@ async fn metadata_irrelevant_events_coalesce_state_db_updated_at() -> std::io::R
let home = TempDir::new().expect("temp dir");
let config = test_config(home.path());
let state_db = StateRuntime::init(home.path().to_path_buf(), config.model_provider_id.clone())
.await
.expect("state db should initialize");
let state_db = StateRuntime::init(
home.path().to_path_buf(),
config.model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await
@@ -639,9 +685,13 @@ async fn shutdown_flushes_pending_metadata_irrelevant_updated_at() -> std::io::R
let home = TempDir::new().expect("temp dir");
let config = test_config(home.path());
let state_db = StateRuntime::init(home.path().to_path_buf(), config.model_provider_id.clone())
.await
.expect("state db should initialize");
let state_db = StateRuntime::init(
home.path().to_path_buf(),
config.model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await
@@ -699,9 +749,13 @@ async fn metadata_irrelevant_events_fall_back_to_upsert_when_thread_missing() ->
let home = TempDir::new().expect("temp dir");
let config = test_config(home.path());
let state_db = StateRuntime::init(home.path().to_path_buf(), config.model_provider_id.clone())
.await
.expect("state db should initialize");
let state_db = StateRuntime::init(
home.path().to_path_buf(),
config.model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let thread_id = ThreadId::new();
let rollout_path = home.path().join("rollout.jsonl");
let builder = ThreadMetadataBuilder::new(
@@ -751,7 +805,7 @@ async fn list_threads_db_disabled_does_not_skip_paginated_items() -> std::io::Re
let default_provider = config.model_provider_id.clone();
let page1 = RolloutRecorder::list_threads(
/*state_db_ctx*/ None,
StateDbAccess::none(),
&config,
/*page_size*/ 1,
/*cursor*/ None,
@@ -769,7 +823,7 @@ async fn list_threads_db_disabled_does_not_skip_paginated_items() -> std::io::Re
let cursor = page1.next_cursor.clone().expect("cursor should be present");
let page2 = RolloutRecorder::list_threads(
/*state_db_ctx*/ None,
StateDbAccess::none(),
&config,
/*page_size*/ 1,
Some(&cursor),
@@ -787,6 +841,62 @@ async fn list_threads_db_disabled_does_not_skip_paginated_items() -> std::io::Re
Ok(())
}
#[tokio::test]
async fn list_threads_records_db_unavailable_fallback_metric() -> std::io::Result<()> {
let home = TempDir::new().expect("temp dir");
let config = test_config(home.path());
let _path = write_session_file(home.path(), "2025-01-03T12-00-00", Uuid::from_u128(9101))?;
let metrics = Arc::new(TestMetrics::default());
let metrics_handle: codex_state::DbMetricsRecorderHandle = metrics.clone();
let state_db_ctx = StateDbAccess::none().with_standalone_metrics(Some(metrics_handle));
let page = RolloutRecorder::list_threads(
state_db_ctx,
&config,
/*page_size*/ 10,
/*cursor*/ None,
ThreadSortKey::CreatedAt,
SortDirection::Desc,
&[],
/*model_providers*/ None,
/*cwd_filters*/ None,
config.model_provider_id.as_str(),
/*search_term*/ None,
)
.await?;
assert_eq!(page.items.len(), 1);
assert!(metrics.has_fallback("list_threads", "db_unavailable"));
Ok(())
}
#[tokio::test]
async fn find_latest_thread_path_records_db_unavailable_fallback_metric() -> std::io::Result<()> {
let home = TempDir::new().expect("temp dir");
let config = test_config(home.path());
let path = write_session_file(home.path(), "2025-01-03T12-00-00", Uuid::from_u128(9103))?;
let metrics = Arc::new(TestMetrics::default());
let metrics_handle: codex_state::DbMetricsRecorderHandle = metrics.clone();
let state_db_ctx = StateDbAccess::none().with_standalone_metrics(Some(metrics_handle));
let found = RolloutRecorder::find_latest_thread_path(
state_db_ctx,
&config,
/*page_size*/ 10,
/*cursor*/ None,
ThreadSortKey::CreatedAt,
&[],
/*model_providers*/ None,
config.model_provider_id.as_str(),
/*filter_cwd*/ None,
)
.await?;
assert_eq!(found, Some(path));
assert!(metrics.has_fallback("find_latest_thread_path", "db_unavailable"));
Ok(())
}
#[tokio::test]
async fn list_threads_db_enabled_drops_missing_rollout_paths() -> std::io::Result<()> {
let home = TempDir::new().expect("temp dir");
@@ -801,6 +911,7 @@ async fn list_threads_db_enabled_drops_missing_rollout_paths() -> std::io::Resul
let runtime = codex_state::StateRuntime::init(
home.path().to_path_buf(),
config.model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
@@ -829,7 +940,7 @@ async fn list_threads_db_enabled_drops_missing_rollout_paths() -> std::io::Resul
let default_provider = config.model_provider_id.clone();
let page = RolloutRecorder::list_threads(
Some(runtime.clone()),
StateDbAccess::new(Some(runtime.clone())),
&config,
/*page_size*/ 10,
/*cursor*/ None,
@@ -866,6 +977,7 @@ async fn list_threads_db_enabled_repairs_stale_rollout_paths() -> std::io::Resul
let runtime = codex_state::StateRuntime::init(
home.path().to_path_buf(),
config.model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
@@ -894,7 +1006,7 @@ async fn list_threads_db_enabled_repairs_stale_rollout_paths() -> std::io::Resul
let default_provider = config.model_provider_id.clone();
let page = RolloutRecorder::list_threads(
Some(runtime.clone()),
StateDbAccess::new(Some(runtime.clone())),
&config,
/*page_size*/ 1,
/*cursor*/ None,
@@ -926,6 +1038,7 @@ async fn list_threads_state_db_only_skips_jsonl_repair_scan() -> std::io::Result
let runtime = codex_state::StateRuntime::init(
home.path().to_path_buf(),
config.model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
@@ -967,7 +1080,7 @@ async fn list_threads_state_db_only_skips_jsonl_repair_scan() -> std::io::Result
let cwd_filters = [home.path().to_path_buf()];
let state_db_only_page = RolloutRecorder::list_threads_from_state_db(
Some(runtime.clone()),
StateDbAccess::new(Some(runtime.clone())),
&config,
/*page_size*/ 10,
/*cursor*/ None,
@@ -983,7 +1096,7 @@ async fn list_threads_state_db_only_skips_jsonl_repair_scan() -> std::io::Result
assert_eq!(state_db_only_page.items.len(), 0);
let repaired_page = RolloutRecorder::list_threads(
Some(runtime.clone()),
StateDbAccess::new(Some(runtime.clone())),
&config,
/*page_size*/ 10,
/*cursor*/ None,
@@ -999,7 +1112,7 @@ async fn list_threads_state_db_only_skips_jsonl_repair_scan() -> std::io::Result
assert_eq!(repaired_page.items.len(), 1);
let repaired_state_db_only_page = RolloutRecorder::list_threads_from_state_db(
Some(runtime.clone()),
StateDbAccess::new(Some(runtime.clone())),
&config,
/*page_size*/ 10,
/*cursor*/ None,
@@ -1029,6 +1142,7 @@ async fn list_threads_default_filter_returns_filesystem_scan_results() -> std::i
let runtime = codex_state::StateRuntime::init(
home.path().to_path_buf(),
config.model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
@@ -1057,7 +1171,7 @@ async fn list_threads_default_filter_returns_filesystem_scan_results() -> std::i
let cwd_filters = [stale_cwd];
let state_db_only_page = RolloutRecorder::list_threads_from_state_db(
Some(runtime.clone()),
StateDbAccess::new(Some(runtime.clone())),
&config,
/*page_size*/ 10,
/*cursor*/ None,
@@ -1073,7 +1187,7 @@ async fn list_threads_default_filter_returns_filesystem_scan_results() -> std::i
assert_eq!(state_db_only_page.items.len(), 1);
let scanned_page = RolloutRecorder::list_threads(
Some(runtime.clone()),
StateDbAccess::new(Some(runtime.clone())),
&config,
/*page_size*/ 10,
/*cursor*/ None,
@@ -1089,7 +1203,7 @@ async fn list_threads_default_filter_returns_filesystem_scan_results() -> std::i
assert_eq!(scanned_page.items.len(), 0);
let repaired_state_db_only_page = RolloutRecorder::list_threads_from_state_db(
Some(runtime.clone()),
StateDbAccess::new(Some(runtime.clone())),
&config,
/*page_size*/ 10,
/*cursor*/ None,
@@ -1118,6 +1232,7 @@ async fn list_threads_metadata_filter_overlays_state_db_list_metadata() -> std::
let runtime = codex_state::StateRuntime::init(
home.path().to_path_buf(),
config.model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
@@ -1148,7 +1263,7 @@ async fn list_threads_metadata_filter_overlays_state_db_list_metadata() -> std::
.expect("state db upsert should succeed");
let page = RolloutRecorder::list_threads(
Some(runtime.clone()),
StateDbAccess::new(Some(runtime.clone())),
&config,
/*page_size*/ 10,
/*cursor*/ None,
@@ -1247,6 +1362,7 @@ async fn list_threads_search_repairs_stale_state_db_hits_before_returning() -> s
let runtime = codex_state::StateRuntime::init(
home.path().to_path_buf(),
config.model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
@@ -1275,7 +1391,7 @@ async fn list_threads_search_repairs_stale_state_db_hits_before_returning() -> s
.expect("state db upsert should succeed");
let stale_state_db_only_page = RolloutRecorder::list_threads_from_state_db(
Some(runtime.clone()),
StateDbAccess::new(Some(runtime.clone())),
&config,
/*page_size*/ 10,
/*cursor*/ None,
@@ -1291,7 +1407,7 @@ async fn list_threads_search_repairs_stale_state_db_hits_before_returning() -> s
assert_eq!(stale_state_db_only_page.items.len(), 1);
let scanned_page = RolloutRecorder::list_threads(
Some(runtime.clone()),
StateDbAccess::new(Some(runtime.clone())),
&config,
/*page_size*/ 10,
/*cursor*/ None,
@@ -1307,7 +1423,7 @@ async fn list_threads_search_repairs_stale_state_db_hits_before_returning() -> s
assert_eq!(scanned_page.items.len(), 0);
let repaired_state_db_only_page = RolloutRecorder::list_threads_from_state_db(
Some(runtime.clone()),
StateDbAccess::new(Some(runtime.clone())),
&config,
/*page_size*/ 10,
/*cursor*/ None,
+2 -1
View File
@@ -7,6 +7,7 @@ use std::io::SeekFrom;
use std::path::Path;
use std::path::PathBuf;
use crate::state_db::StateDbAccess;
use codex_protocol::ThreadId;
use codex_protocol::protocol::SessionMetaLine;
use serde::Deserialize;
@@ -117,7 +118,7 @@ pub async fn find_thread_names_by_ids(
pub async fn find_thread_meta_by_name_str(
codex_home: &Path,
name: &str,
state_db_ctx: Option<&codex_state::StateRuntime>,
state_db_ctx: &StateDbAccess,
) -> std::io::Result<Option<(PathBuf, SessionMetaLine)>> {
if name.trim().is_empty() {
return Ok(None);
+6 -3
View File
@@ -100,7 +100,8 @@ async fn find_thread_meta_by_name_str_skips_newest_entry_without_rollout() -> st
];
write_index(&path, &lines)?;
let found = find_thread_meta_by_name_str(temp.path(), "same", /*state_db_ctx*/ None).await?;
let found =
find_thread_meta_by_name_str(temp.path(), "same", &crate::StateDbAccess::none()).await?;
assert_eq!(
found.map(|(path, session_meta)| (path, session_meta.meta.id)),
@@ -137,7 +138,8 @@ async fn find_thread_meta_by_name_str_skips_partial_rollout() -> std::io::Result
];
write_index(&path, &lines)?;
let found = find_thread_meta_by_name_str(temp.path(), "same", /*state_db_ctx*/ None).await?;
let found =
find_thread_meta_by_name_str(temp.path(), "same", &crate::StateDbAccess::none()).await?;
assert_eq!(found.map(|(path, _)| path), Some(saved_rollout_path));
Ok(())
@@ -175,7 +177,8 @@ async fn find_thread_meta_by_name_str_ignores_historical_name_after_rename() ->
];
write_index(&path, &lines)?;
let found = find_thread_meta_by_name_str(temp.path(), "same", /*state_db_ctx*/ None).await?;
let found =
find_thread_meta_by_name_str(temp.path(), "same", &crate::StateDbAccess::none()).await?;
assert_eq!(found.map(|(path, _)| path), Some(current_rollout_path));
Ok(())
+43
View File
@@ -0,0 +1,43 @@
use std::sync::Arc;
use std::time::Duration;
use codex_otel::ORIGINATOR_TAG;
use codex_otel::bounded_originator_tag_value;
use codex_state::DbMetricsRecorder;
use codex_state::DbMetricsRecorderHandle;
struct OtelDbMetrics {
metrics: codex_otel::MetricsClient,
originator: &'static str,
}
impl DbMetricsRecorder for OtelDbMetrics {
fn counter(&self, name: &str, inc: i64, tags: &[(&str, &str)]) {
let tags = sqlite_originator_tags(tags, self.originator);
let _ = self.metrics.counter(name, inc, &tags);
}
fn record_duration(&self, name: &str, duration: Duration, tags: &[(&str, &str)]) {
let tags = sqlite_originator_tags(tags, self.originator);
let _ = self.metrics.record_duration(name, duration, &tags);
}
}
pub(crate) fn recorder(
metrics: codex_otel::MetricsClient,
originator: &str,
) -> DbMetricsRecorderHandle {
Arc::new(OtelDbMetrics {
metrics,
originator: bounded_originator_tag_value(originator),
})
}
fn sqlite_originator_tags<'a>(
tags: &[(&'a str, &'a str)],
originator: &'static str,
) -> Vec<(&'a str, &'a str)> {
let mut tags = tags.to_vec();
tags.push((ORIGINATOR_TAG, originator));
tags
}
+162 -41
View File
@@ -4,6 +4,7 @@ use crate::list::Cursor;
use crate::list::SortDirection;
use crate::list::ThreadSortKey;
use crate::metadata;
use crate::sqlite_metrics;
use chrono::DateTime;
use chrono::Utc;
use codex_protocol::ThreadId;
@@ -25,6 +26,61 @@ use tracing::warn;
/// Core-facing handle to the SQLite-backed state runtime.
pub type StateDbHandle = Arc<codex_state::StateRuntime>;
/// Access to SQLite-backed state plus fallback telemetry for local rollout operations.
///
/// Some fallback metrics need to be recorded after SQLite initialization fails,
/// so callers must carry the metrics sink independently from the runtime handle.
#[derive(Clone)]
pub struct StateDbAccess {
state_db: Option<StateDbHandle>,
standalone_metrics: Option<codex_state::DbMetricsRecorderHandle>,
}
impl StateDbAccess {
/// Construct an access bundle with no DB handle and no fallback metrics recorder.
pub fn none() -> Self {
Self {
state_db: None,
standalone_metrics: None,
}
}
pub fn new(state_db: Option<StateDbHandle>) -> Self {
Self {
state_db,
standalone_metrics: None,
}
}
pub fn with_standalone_metrics(
mut self,
standalone_metrics: Option<codex_state::DbMetricsRecorderHandle>,
) -> Self {
self.standalone_metrics = standalone_metrics;
self
}
pub fn state_db(&self) -> Option<StateDbHandle> {
self.state_db.clone()
}
pub fn as_deref(&self) -> Option<&codex_state::StateRuntime> {
self.state_db.as_deref()
}
pub fn is_available(&self) -> bool {
self.state_db.is_some()
}
pub fn metrics(&self) -> Option<&dyn codex_state::DbMetricsRecorder> {
self.standalone_metrics.as_deref().or_else(|| {
self.state_db
.as_deref()
.and_then(codex_state::StateRuntime::metrics)
})
}
}
#[cfg(not(test))]
const STARTUP_BACKFILL_POLL_INTERVAL: Duration = Duration::from_secs(1);
#[cfg(test)]
@@ -39,12 +95,16 @@ const STARTUP_BACKFILL_WAIT_TIMEOUT: Duration = Duration::from_secs(2);
/// This is the process entry point for local state: it opens the SQLite-backed
/// runtime, applies rollout metadata backfills as needed, and returns the
/// initialized handle.
pub async fn init(config: &impl RolloutConfigView) -> Option<StateDbHandle> {
pub async fn init(
config: &impl RolloutConfigView,
metrics: Option<codex_state::DbMetricsRecorderHandle>,
) -> Option<StateDbHandle> {
let config = RolloutConfig::from_view(config);
match try_init_with_roots(
config.codex_home,
config.sqlite_home,
config.model_provider_id,
metrics,
)
.await
{
@@ -60,12 +120,16 @@ pub async fn init(config: &impl RolloutConfigView) -> Option<StateDbHandle> {
///
/// Prefer [`init`] unless the caller needs to surface the exact failure after
/// tracing or UI setup has completed.
pub async fn try_init(config: &impl RolloutConfigView) -> anyhow::Result<StateDbHandle> {
pub async fn try_init(
config: &impl RolloutConfigView,
metrics: Option<codex_state::DbMetricsRecorderHandle>,
) -> anyhow::Result<StateDbHandle> {
let config = RolloutConfig::from_view(config);
try_init_with_roots(
config.codex_home,
config.sqlite_home,
config.model_provider_id,
metrics,
)
.await
}
@@ -74,11 +138,13 @@ async fn try_init_with_roots(
codex_home: PathBuf,
sqlite_home: PathBuf,
default_model_provider_id: String,
metrics: Option<codex_state::DbMetricsRecorderHandle>,
) -> anyhow::Result<StateDbHandle> {
try_init_with_roots_inner(
codex_home,
sqlite_home,
default_model_provider_id,
metrics,
/*backfill_lease_seconds*/ None,
)
.await
@@ -95,6 +161,7 @@ async fn try_init_with_roots_and_backfill_lease(
codex_home,
sqlite_home,
default_model_provider_id,
/*metrics*/ None,
Some(backfill_lease_seconds),
)
.await
@@ -104,54 +171,82 @@ async fn try_init_with_roots_inner(
codex_home: PathBuf,
sqlite_home: PathBuf,
default_model_provider_id: String,
metrics: Option<codex_state::DbMetricsRecorderHandle>,
backfill_lease_seconds: Option<i64>,
) -> anyhow::Result<StateDbHandle> {
let runtime =
codex_state::StateRuntime::init(sqlite_home.clone(), default_model_provider_id.clone())
.await
.map_err(|err| {
anyhow::anyhow!(
"failed to initialize state runtime at {}: {err}",
sqlite_home.display()
)
})?;
let runtime = codex_state::StateRuntime::init(
sqlite_home.clone(),
default_model_provider_id.clone(),
metrics.clone(),
)
.await
.map_err(|err| {
anyhow::anyhow!(
"failed to initialize state runtime at {}: {err}",
sqlite_home.display()
)
})?;
let backfill_gate_started = Instant::now();
let backfill_gate_result = wait_for_startup_backfill(
runtime.as_ref(),
codex_home.as_path(),
default_model_provider_id.as_str(),
backfill_lease_seconds,
)
.await;
codex_state::record_db_init_backfill_gate_metric(
metrics.as_deref(),
backfill_gate_started.elapsed(),
&backfill_gate_result,
);
backfill_gate_result?;
Ok(runtime)
}
async fn wait_for_startup_backfill(
runtime: &codex_state::StateRuntime,
codex_home: &Path,
default_model_provider_id: &str,
backfill_lease_seconds: Option<i64>,
) -> anyhow::Result<()> {
let wait_started = Instant::now();
let mut reported_wait = false;
loop {
let backfill_state = runtime.get_backfill_state().await.map_err(|err| {
anyhow::anyhow!(
"failed to read backfill state at {}: {err}",
codex_home.display()
)
})?;
let backfill_state = match runtime.get_backfill_state().await {
Ok(state) => state,
Err(err) => {
return Err(anyhow::anyhow!(
"failed to read backfill state at {}: {err}",
codex_home.display()
));
}
};
if backfill_state.status == codex_state::BackfillStatus::Complete {
return Ok(runtime);
return Ok(());
}
if let Some(backfill_lease_seconds) = backfill_lease_seconds {
metadata::backfill_sessions_with_lease(
runtime.as_ref(),
codex_home.as_path(),
default_model_provider_id.as_str(),
runtime,
codex_home,
default_model_provider_id,
backfill_lease_seconds,
)
.await;
} else {
metadata::backfill_sessions(
runtime.as_ref(),
codex_home.as_path(),
default_model_provider_id.as_str(),
)
.await;
metadata::backfill_sessions(runtime, codex_home, default_model_provider_id).await;
}
let backfill_state = runtime.get_backfill_state().await.map_err(|err| {
anyhow::anyhow!(
"failed to read backfill state at {} after startup backfill: {err}",
codex_home.display()
)
})?;
let backfill_state = match runtime.get_backfill_state().await {
Ok(state) => state,
Err(err) => {
return Err(anyhow::anyhow!(
"failed to read backfill state at {} after startup backfill: {err}",
codex_home.display()
));
}
};
if backfill_state.status == codex_state::BackfillStatus::Complete {
return Ok(runtime);
return Ok(());
}
if wait_started.elapsed() >= STARTUP_BACKFILL_WAIT_TIMEOUT {
return Err(anyhow::anyhow!(
@@ -192,39 +287,65 @@ fn emit_startup_warning(message: &str) {
///
/// Unlike [`init`], this helper does not run rollout backfill. It is for
/// optional local reads from non-owning contexts such as remote app-server mode.
pub async fn get_state_db(config: &impl RolloutConfigView) -> Option<StateDbHandle> {
pub async fn get_state_db(
config: &impl RolloutConfigView,
metrics: Option<codex_state::DbMetricsRecorderHandle>,
) -> Option<StateDbHandle> {
let state_path = codex_state::state_db_path(config.sqlite_home());
if !tokio::fs::try_exists(&state_path).await.unwrap_or(false) {
codex_state::record_db_fallback_metric(
metrics.as_deref(),
"get_state_db",
"db_unavailable",
);
return None;
}
let runtime = codex_state::StateRuntime::init(
let runtime = match codex_state::StateRuntime::init(
config.sqlite_home().to_path_buf(),
config.model_provider_id().to_string(),
metrics.clone(),
)
.await
.ok()?;
require_backfill_complete(runtime, config.sqlite_home()).await
{
Ok(runtime) => runtime,
Err(_) => {
codex_state::record_db_fallback_metric(metrics.as_deref(), "get_state_db", "db_error");
return None;
}
};
require_backfill_complete(runtime, config.sqlite_home(), metrics.as_deref()).await
}
/// Build a SQLite metrics recorder backed by an OTEL metrics client.
pub fn otel_db_metrics_recorder(
metrics: codex_otel::MetricsClient,
originator: &str,
) -> codex_state::DbMetricsRecorderHandle {
sqlite_metrics::recorder(metrics, originator)
}
async fn require_backfill_complete(
runtime: StateDbHandle,
codex_home: &Path,
sqlite_home: &Path,
metrics: Option<&dyn codex_state::DbMetricsRecorder>,
) -> Option<StateDbHandle> {
match runtime.get_backfill_state().await {
Ok(state) if state.status == codex_state::BackfillStatus::Complete => Some(runtime),
Ok(state) => {
warn!(
"state db backfill not complete at {} (status: {})",
codex_home.display(),
sqlite_home.display(),
state.status.as_str()
);
codex_state::record_db_fallback_metric(metrics, "get_state_db", "backfill_incomplete");
None
}
Err(err) => {
warn!(
"failed to read backfill state at {}: {err}",
codex_home.display()
sqlite_home.display()
);
codex_state::record_db_fallback_metric(metrics, "get_state_db", "db_error");
None
}
}
+12 -6
View File
@@ -27,9 +27,12 @@ fn cursor_to_anchor_normalizes_timestamp_format() {
#[tokio::test]
async fn try_init_waits_for_concurrent_startup_backfill() -> anyhow::Result<()> {
let home = TempDir::new().expect("temp dir");
let runtime =
codex_state::StateRuntime::init(home.path().to_path_buf(), "test-provider".to_string())
.await?;
let runtime = codex_state::StateRuntime::init(
home.path().to_path_buf(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await?;
let claimed = runtime.try_claim_backfill(/*lease_seconds*/ 60).await?;
assert!(claimed);
let runtime_for_completion = runtime.clone();
@@ -59,9 +62,12 @@ async fn try_init_waits_for_concurrent_startup_backfill() -> anyhow::Result<()>
#[tokio::test]
async fn try_init_times_out_waiting_for_stuck_startup_backfill() -> anyhow::Result<()> {
let home = TempDir::new().expect("temp dir");
let runtime =
codex_state::StateRuntime::init(home.path().to_path_buf(), "test-provider".to_string())
.await?;
let runtime = codex_state::StateRuntime::init(
home.path().to_path_buf(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await?;
let claimed = runtime.try_claim_backfill(/*lease_seconds*/ 60).await?;
assert!(claimed);
+50 -21
View File
@@ -19,6 +19,7 @@ use time::macros::format_description;
use uuid::Uuid;
use crate::INTERACTIVE_SESSION_SOURCES;
use crate::StateDbAccess;
use crate::find_thread_path_by_id_str;
use crate::list::Cursor;
use crate::list::ThreadItem;
@@ -59,9 +60,13 @@ async fn insert_state_db_thread(
rollout_path: &Path,
archived: bool,
) -> crate::state_db::StateDbHandle {
let runtime = codex_state::StateRuntime::init(home.to_path_buf(), TEST_PROVIDER.to_string())
.await
.expect("state db should initialize");
let runtime = codex_state::StateRuntime::init(
home.to_path_buf(),
TEST_PROVIDER.to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
runtime
.mark_backfill_complete(/*last_watermark*/ None)
.await
@@ -245,9 +250,13 @@ async fn find_thread_path_falls_back_when_db_path_is_stale() {
)
.await;
let found = find_thread_path_by_id_str(home, &uuid.to_string(), Some(runtime.as_ref()))
.await
.expect("lookup should succeed");
let found = find_thread_path_by_id_str(
home,
&uuid.to_string(),
&StateDbAccess::new(Some(runtime.clone())),
)
.await
.expect("lookup should succeed");
assert_eq!(found, Some(fs_rollout_path.clone()));
assert_state_db_rollout_path(home, thread_id, Some(fs_rollout_path.as_path())).await;
}
@@ -290,9 +299,13 @@ async fn find_thread_path_falls_back_when_db_path_points_to_another_thread() {
)
.await;
let found = find_thread_path_by_id_str(home, &uuid.to_string(), Some(runtime.as_ref()))
.await
.expect("lookup should succeed");
let found = find_thread_path_by_id_str(
home,
&uuid.to_string(),
&StateDbAccess::new(Some(runtime.clone())),
)
.await
.expect("lookup should succeed");
assert_eq!(found, Some(fs_rollout_path.clone()));
assert_state_db_rollout_path(home, thread_id, Some(fs_rollout_path.as_path())).await;
}
@@ -315,17 +328,25 @@ async fn find_thread_path_repairs_missing_db_row_after_filesystem_fallback() {
let fs_rollout_path = home.join(format!("sessions/2025/01/03/rollout-{ts}-{uuid}.jsonl"));
// Create an empty state DB so lookup takes the DB-first path and then falls back to files.
let runtime = codex_state::StateRuntime::init(home.to_path_buf(), TEST_PROVIDER.to_string())
.await
.expect("state db should initialize");
let runtime = codex_state::StateRuntime::init(
home.to_path_buf(),
TEST_PROVIDER.to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
runtime
.mark_backfill_complete(/*last_watermark*/ None)
.await
.expect("backfill should be complete");
let found = find_thread_path_by_id_str(home, &uuid.to_string(), Some(runtime.as_ref()))
.await
.expect("lookup should succeed");
let found = find_thread_path_by_id_str(
home,
&uuid.to_string(),
&StateDbAccess::new(Some(runtime.clone())),
)
.await
.expect("lookup should succeed");
assert_eq!(found, Some(fs_rollout_path.clone()));
assert_state_db_rollout_path(home, thread_id, Some(fs_rollout_path.as_path())).await;
}
@@ -347,9 +368,13 @@ async fn find_thread_path_accepts_existing_state_db_path_without_canonical_filen
)
.await;
let found = find_thread_path_by_id_str(home, &uuid.to_string(), Some(runtime.as_ref()))
.await
.expect("lookup should succeed");
let found = find_thread_path_by_id_str(
home,
&uuid.to_string(),
&StateDbAccess::new(Some(runtime.clone())),
)
.await
.expect("lookup should succeed");
assert_eq!(found, Some(db_rollout_path));
}
@@ -368,9 +393,13 @@ async fn assert_state_db_rollout_path(
thread_id: ThreadId,
expected_path: Option<&Path>,
) {
let runtime = codex_state::StateRuntime::init(home.to_path_buf(), TEST_PROVIDER.to_string())
.await
.expect("state db should initialize");
let runtime = codex_state::StateRuntime::init(
home.to_path_buf(),
TEST_PROVIDER.to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let path = runtime
.find_rollout_path_by_id(thread_id, Some(false))
.await
+2 -1
View File
@@ -111,7 +111,8 @@ async fn main() -> anyhow::Result<()> {
.parent()
.map(ToOwned::to_owned)
.unwrap_or_else(|| PathBuf::from("."));
let runtime = StateRuntime::init(codex_home, "logs-client".to_string()).await?;
let runtime =
StateRuntime::init(codex_home, "logs-client".to_string(), /*metrics*/ None).await?;
let mut last_id =
print_backfill(runtime.as_ref(), &filter, args.backfill, args.compact).await?;
+26 -1
View File
@@ -10,12 +10,13 @@ mod migrations;
mod model;
mod paths;
mod runtime;
mod telemetry;
pub use model::LogEntry;
pub use model::LogQuery;
pub use model::LogRow;
pub use model::Phase2JobClaimOutcome;
/// Preferred entrypoint: owns configuration and metrics.
/// Preferred entrypoint: owns SQLite configuration and optional metrics injection.
pub use runtime::StateRuntime;
/// Low-level storage engine: useful for focused tests.
@@ -56,6 +57,8 @@ pub use runtime::logs_db_filename;
pub use runtime::logs_db_path;
pub use runtime::state_db_filename;
pub use runtime::state_db_path;
pub use telemetry::DbMetricsRecorder;
pub use telemetry::DbMetricsRecorderHandle;
/// Environment variable for overriding the SQLite state database home directory.
pub const SQLITE_HOME_ENV: &str = "CODEX_SQLITE_HOME";
@@ -71,3 +74,25 @@ pub const DB_ERROR_METRIC: &str = "codex.db.error";
pub const DB_METRIC_BACKFILL: &str = "codex.db.backfill";
/// Metrics on backfill duration. Tags: [status]
pub const DB_METRIC_BACKFILL_DURATION_MS: &str = "codex.db.backfill.duration_ms";
/// SQLite startup initialization attempts. Tags: [status, phase, db, error]
pub const DB_INIT_METRIC: &str = "codex.sqlite.init.count";
/// SQLite startup initialization duration. Tags: [status, phase, db, error]
pub const DB_INIT_DURATION_METRIC: &str = "codex.sqlite.init.duration_ms";
/// Filesystem fallback after SQLite could not serve a request. Tags: [caller, reason]
pub const DB_FALLBACK_METRIC: &str = "codex.sqlite.fallback.count";
pub fn record_db_fallback_metric(
metrics: Option<&dyn DbMetricsRecorder>,
caller: &'static str,
reason: &'static str,
) {
telemetry::record_fallback(metrics, caller, reason);
}
pub fn record_db_init_backfill_gate_metric(
metrics: Option<&dyn DbMetricsRecorder>,
duration: std::time::Duration,
result: &anyhow::Result<()>,
) {
telemetry::record_init_backfill_gate(metrics, duration, result);
}
+28 -12
View File
@@ -547,9 +547,13 @@ mod tests {
#[tokio::test]
async fn sqlite_feedback_logs_match_feedback_formatter_shape() {
let codex_home = temp_codex_home();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let writer = SharedWriter::default();
let layer = start(runtime.clone());
@@ -605,9 +609,13 @@ mod tests {
#[tokio::test]
async fn flush_persists_logs_for_query() {
let codex_home = temp_codex_home();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let layer = start(runtime.clone());
let guard = tracing_subscriber::registry()
@@ -636,9 +644,13 @@ mod tests {
#[tokio::test]
async fn configured_batch_size_flushes_without_explicit_flush() {
let codex_home = temp_codex_home();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let layer = LogDbLayer::start_with_config(
runtime.clone(),
LogSinkQueueConfig {
@@ -685,9 +697,13 @@ mod tests {
#[tokio::test]
async fn configured_flush_interval_persists_buffered_logs() {
let codex_home = temp_codex_home();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let layer = LogDbLayer::start_with_config(
runtime.clone(),
LogSinkQueueConfig {
+122 -21
View File
@@ -27,6 +27,9 @@ use crate::model::datetime_to_epoch_millis;
use crate::model::datetime_to_epoch_seconds;
use crate::model::epoch_millis_to_datetime;
use crate::paths::file_modified_time_utc;
use crate::telemetry::DbKind;
use crate::telemetry::DbMetricsRecorder;
use crate::telemetry::DbMetricsRecorderHandle;
use chrono::DateTime;
use chrono::Utc;
use codex_protocol::ThreadId;
@@ -52,6 +55,7 @@ use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::AtomicI64;
use std::time::Duration;
use std::time::Instant;
use tracing::warn;
mod agent_jobs;
@@ -85,6 +89,7 @@ pub struct StateRuntime {
default_provider: String,
pool: Arc<sqlx::SqlitePool>,
logs_pool: Arc<sqlx::SqlitePool>,
metrics: Option<DbMetricsRecorderHandle>,
thread_updated_at_millis: Arc<AtomicI64>,
}
@@ -93,8 +98,13 @@ impl StateRuntime {
///
/// This opens (and migrates) the SQLite databases under `codex_home`,
/// keeping logs in a dedicated file to reduce lock contention with the
/// rest of the state store.
pub async fn init(codex_home: PathBuf, default_provider: String) -> anyhow::Result<Arc<Self>> {
/// rest of the state store. Callers may provide a metrics sink for SQLite
/// startup telemetry.
pub async fn init(
codex_home: PathBuf,
default_provider: String,
metrics: Option<DbMetricsRecorderHandle>,
) -> anyhow::Result<Arc<Self>> {
tokio::fs::create_dir_all(&codex_home).await?;
let state_migrator = runtime_state_migrator();
let logs_migrator = runtime_logs_migrator();
@@ -116,28 +126,50 @@ impl StateRuntime {
.await;
let state_path = state_db_path(codex_home.as_path());
let logs_path = logs_db_path(codex_home.as_path());
let pool = match open_state_sqlite(&state_path, &state_migrator).await {
let pool = match open_state_sqlite(&state_path, &state_migrator, metrics.as_deref()).await {
Ok(db) => Arc::new(db),
Err(err) => {
warn!("failed to open state db at {}: {err}", state_path.display());
return Err(err);
}
};
let logs_pool = match open_logs_sqlite(&logs_path, &logs_migrator).await {
let logs_pool = match open_logs_sqlite(&logs_path, &logs_migrator, metrics.as_deref()).await
{
Ok(db) => Arc::new(db),
Err(err) => {
warn!("failed to open logs db at {}: {err}", logs_path.display());
return Err(err);
}
};
let thread_updated_at_millis: Option<i64> =
let started = Instant::now();
let backfill_state_result = ensure_backfill_state_row_in_pool(pool.as_ref()).await;
crate::telemetry::record_init_result(
metrics.as_deref(),
DbKind::State,
"ensure_backfill_state",
started.elapsed(),
&backfill_state_result,
);
backfill_state_result?;
let started = Instant::now();
let thread_updated_at_millis_result: anyhow::Result<Option<i64>> =
sqlx::query_scalar("SELECT MAX(threads.updated_at_ms) FROM threads")
.fetch_one(pool.as_ref())
.await?;
.await
.map_err(anyhow::Error::from);
crate::telemetry::record_init_result(
metrics.as_deref(),
DbKind::State,
"post_init_query",
started.elapsed(),
&thread_updated_at_millis_result,
);
let thread_updated_at_millis = thread_updated_at_millis_result?;
let thread_updated_at_millis = thread_updated_at_millis.unwrap_or(0);
let runtime = Arc::new(Self {
pool,
logs_pool,
metrics,
codex_home,
default_provider,
thread_updated_at_millis: Arc::new(AtomicI64::new(thread_updated_at_millis)),
@@ -155,6 +187,11 @@ impl StateRuntime {
pub fn codex_home(&self) -> &Path {
self.codex_home.as_path()
}
/// Return the explicit metrics recorder attached to this runtime, if any.
pub fn metrics(&self) -> Option<&dyn DbMetricsRecorder> {
self.metrics.as_deref()
}
}
fn base_sqlite_options(path: &Path) -> SqliteConnectOptions {
@@ -167,27 +204,87 @@ fn base_sqlite_options(path: &Path) -> SqliteConnectOptions {
.log_statements(LevelFilter::Off)
}
async fn open_state_sqlite(path: &Path, migrator: &Migrator) -> anyhow::Result<SqlitePool> {
async fn open_state_sqlite(
path: &Path,
migrator: &Migrator,
metrics: Option<&dyn DbMetricsRecorder>,
) -> anyhow::Result<SqlitePool> {
// New state DBs should use incremental auto-vacuum, but retrofitting an
// existing DB requires a full VACUUM. Do not attempt that during process
// startup: it is maintenance work that can contend with foreground writers.
open_sqlite(
path,
migrator,
metrics,
DbKind::State,
"open_state",
"migrate_state",
)
.await
}
async fn open_logs_sqlite(
path: &Path,
migrator: &Migrator,
metrics: Option<&dyn DbMetricsRecorder>,
) -> anyhow::Result<SqlitePool> {
open_sqlite(
path,
migrator,
metrics,
DbKind::Logs,
"open_logs",
"migrate_logs",
)
.await
}
async fn open_sqlite(
path: &Path,
migrator: &Migrator,
metrics: Option<&dyn DbMetricsRecorder>,
db: DbKind,
open_phase: &'static str,
migrate_phase: &'static str,
) -> anyhow::Result<SqlitePool> {
let options = base_sqlite_options(path).auto_vacuum(SqliteAutoVacuum::Incremental);
let pool = SqlitePoolOptions::new()
let started = Instant::now();
let pool_result = SqlitePoolOptions::new()
.max_connections(5)
.acquire_slow_level(LevelFilter::Warn)
.acquire_slow_threshold(Duration::from_millis(250))
.connect_with(options)
.await?;
migrator.run(&pool).await?;
.await
.map_err(anyhow::Error::from);
crate::telemetry::record_init_result(metrics, db, open_phase, started.elapsed(), &pool_result);
let pool = pool_result?;
let started = Instant::now();
let migrate_result = migrator.run(&pool).await.map_err(anyhow::Error::from);
crate::telemetry::record_init_result(
metrics,
db,
migrate_phase,
started.elapsed(),
&migrate_result,
);
migrate_result?;
Ok(pool)
}
async fn open_logs_sqlite(path: &Path, migrator: &Migrator) -> anyhow::Result<SqlitePool> {
let options = base_sqlite_options(path).auto_vacuum(SqliteAutoVacuum::Incremental);
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect_with(options)
.await?;
migrator.run(&pool).await?;
Ok(pool)
async fn ensure_backfill_state_row_in_pool(pool: &sqlx::SqlitePool) -> anyhow::Result<()> {
sqlx::query(
r#"
INSERT INTO backfill_state (id, status, last_watermark, last_success_at, updated_at)
VALUES (?, ?, NULL, NULL, ?)
ON CONFLICT(id) DO NOTHING
"#,
)
.bind(1_i64)
.bind(crate::BackfillStatus::Pending.as_str())
.bind(Utc::now().timestamp())
.execute(pool)
.await?;
Ok(())
}
fn db_filename(base_name: &str, version: u32) -> String {
@@ -355,9 +452,13 @@ mod tests {
strict_pool.close().await;
let tolerant_migrator = runtime_state_migrator();
let tolerant_pool = open_state_sqlite(state_path.as_path(), &tolerant_migrator)
.await
.expect("runtime migrator should tolerate newer applied migrations");
let tolerant_pool = open_state_sqlite(
state_path.as_path(),
&tolerant_migrator,
/*metrics*/ None,
)
.await
.expect("runtime migrator should tolerate newer applied migrations");
tolerant_pool.close().await;
let _ = tokio::fs::remove_dir_all(codex_home).await;
+12 -2
View File
@@ -615,7 +615,12 @@ mod tests {
#[tokio::test]
async fn report_agent_job_item_result_completes_item_atomically() -> anyhow::Result<()> {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home, "test-provider".to_string()).await?;
let runtime = StateRuntime::init(
codex_home,
"test-provider".to_string(),
/*metrics*/ None,
)
.await?;
let (job_id, item_id, thread_id) = create_running_single_item_job(runtime.as_ref()).await?;
let accepted = runtime
@@ -655,7 +660,12 @@ mod tests {
#[tokio::test]
async fn report_agent_job_item_result_rejects_late_reports() -> anyhow::Result<()> {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home, "test-provider".to_string()).await?;
let runtime = StateRuntime::init(
codex_home,
"test-provider".to_string(),
/*metrics*/ None,
)
.await?;
let (job_id, item_id, thread_id) = create_running_single_item_job(runtime.as_ref()).await?;
let marked_failed = runtime
+21 -10
View File
@@ -2,7 +2,6 @@ use super::*;
impl StateRuntime {
pub async fn get_backfill_state(&self) -> anyhow::Result<crate::BackfillState> {
self.ensure_backfill_state_row().await?;
let row = sqlx::query(
r#"
SELECT status, last_watermark, last_success_at
@@ -160,9 +159,13 @@ mod tests {
.await
.expect("write numeric");
let _runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let _runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
for suffix in ["", "-wal", "-shm", "-journal"] {
let legacy_path = codex_home.join(format!("{unversioned_name}{suffix}"));
@@ -207,9 +210,13 @@ mod tests {
#[tokio::test]
async fn backfill_state_persists_progress_and_completion() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let initial = runtime
.get_backfill_state()
@@ -260,9 +267,13 @@ mod tests {
#[tokio::test]
async fn backfill_claim_is_singleton_until_stale_and_blocked_when_complete() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let claimed = runtime
.try_claim_backfill(/*lease_seconds*/ 3600)
+7 -3
View File
@@ -431,9 +431,13 @@ mod tests {
use pretty_assertions::assert_eq;
async fn test_runtime() -> std::sync::Arc<StateRuntime> {
StateRuntime::init(unique_temp_dir(), "test-provider".to_string())
.await
.expect("state db should initialize")
StateRuntime::init(
unique_temp_dir(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize")
}
fn test_thread_id() -> ThreadId {
+154 -66
View File
@@ -573,9 +573,13 @@ mod tests {
#[tokio::test]
async fn insert_logs_use_dedicated_log_database() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
runtime
.insert_logs(&[LogEntry {
@@ -645,9 +649,13 @@ mod tests {
pool.close().await;
drop(pool);
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let rows = runtime
.query_logs(&LogQuery::default())
@@ -736,9 +744,13 @@ mod tests {
pool.close().await;
drop(pool);
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
assert!(
!legacy_logs_path.exists(),
@@ -762,9 +774,13 @@ mod tests {
#[tokio::test]
async fn init_configures_logs_db_with_incremental_auto_vacuum() {
let codex_home = unique_temp_dir();
let _runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let _runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let pool = open_db_pool(logs_db_path(codex_home.as_path()).as_path()).await;
let auto_vacuum = sqlx::query_scalar::<_, i64>("PRAGMA auto_vacuum")
@@ -806,9 +822,13 @@ mod tests {
#[tokio::test]
async fn query_logs_with_search_matches_rendered_body_substring() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
runtime
.insert_logs(&[
@@ -859,9 +879,13 @@ mod tests {
#[tokio::test]
async fn query_logs_filters_level_set_without_rewriting_stored_level() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
runtime
.insert_logs(&[
@@ -944,9 +968,13 @@ mod tests {
#[tokio::test]
async fn insert_logs_prunes_old_rows_when_thread_exceeds_size_limit() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let six_mebibytes = "a".repeat(6 * 1024 * 1024);
runtime
@@ -998,9 +1026,13 @@ mod tests {
#[tokio::test]
async fn insert_logs_prunes_single_thread_row_when_it_exceeds_size_limit() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let eleven_mebibytes = "d".repeat(11 * 1024 * 1024);
runtime
@@ -1036,9 +1068,13 @@ mod tests {
#[tokio::test]
async fn insert_logs_prunes_threadless_rows_per_process_uuid_only() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let six_mebibytes = "b".repeat(6 * 1024 * 1024);
runtime
@@ -1105,9 +1141,13 @@ mod tests {
#[tokio::test]
async fn insert_logs_prunes_single_threadless_process_row_when_it_exceeds_size_limit() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let eleven_mebibytes = "e".repeat(11 * 1024 * 1024);
runtime
@@ -1143,9 +1183,13 @@ mod tests {
#[tokio::test]
async fn insert_logs_prunes_threadless_rows_with_null_process_uuid() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let six_mebibytes = "c".repeat(6 * 1024 * 1024);
runtime
@@ -1211,9 +1255,13 @@ mod tests {
#[tokio::test]
async fn insert_logs_prunes_single_threadless_null_process_row_when_it_exceeds_limit() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let eleven_mebibytes = "f".repeat(11 * 1024 * 1024);
runtime
@@ -1249,9 +1297,13 @@ mod tests {
#[tokio::test]
async fn insert_logs_prunes_old_rows_when_thread_exceeds_row_limit() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let entries: Vec<LogEntry> = (1..=1_001)
.map(|ts| LogEntry {
@@ -1292,9 +1344,13 @@ mod tests {
#[tokio::test]
async fn insert_logs_prunes_old_threadless_rows_when_process_exceeds_row_limit() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let entries: Vec<LogEntry> = (1..=1_001)
.map(|ts| LogEntry {
@@ -1339,9 +1395,13 @@ mod tests {
#[tokio::test]
async fn insert_logs_prunes_old_threadless_null_process_rows_when_row_limit_exceeded() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let entries: Vec<LogEntry> = (1..=1_001)
.map(|ts| LogEntry {
@@ -1386,9 +1446,13 @@ mod tests {
#[tokio::test]
async fn query_feedback_logs_returns_newest_lines_within_limit_in_order() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
runtime
.insert_logs(&[
@@ -1456,9 +1520,13 @@ mod tests {
#[tokio::test]
async fn query_feedback_logs_excludes_oversized_newest_row() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let eleven_mebibytes = "z".repeat(11 * 1024 * 1024);
runtime
@@ -1506,9 +1574,13 @@ mod tests {
#[tokio::test]
async fn query_feedback_logs_includes_threadless_rows_from_same_process() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
runtime
.insert_logs(&[
@@ -1604,9 +1676,13 @@ mod tests {
#[tokio::test]
async fn query_feedback_logs_excludes_threadless_rows_from_prior_processes() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
runtime
.insert_logs(&[
@@ -1702,9 +1778,13 @@ mod tests {
#[tokio::test]
async fn query_feedback_logs_keeps_newest_suffix_across_thread_and_threadless_logs() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_marker = "thread-scoped-oldest";
let threadless_older_marker = "threadless-older";
let threadless_newer_marker = "threadless-newer";
@@ -1777,9 +1857,13 @@ mod tests {
#[tokio::test]
async fn query_feedback_logs_for_threads_merges_requested_threads_and_threadless_rows() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
runtime
.insert_logs(&[
@@ -1897,9 +1981,13 @@ mod tests {
#[tokio::test]
async fn query_feedback_logs_for_threads_returns_empty_for_empty_thread_list() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let bytes = runtime
.query_feedback_logs_for_threads(&[])
+266 -114
View File
@@ -1308,9 +1308,13 @@ mod tests {
#[tokio::test]
async fn stage1_claim_skips_when_up_to_date() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
let metadata = test_thread_metadata(&codex_home, thread_id, codex_home.join("a"));
@@ -1376,9 +1380,13 @@ mod tests {
#[tokio::test]
async fn stage1_running_stale_can_be_stolen_but_fresh_running_is_skipped() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
@@ -1431,9 +1439,13 @@ mod tests {
#[tokio::test]
async fn stage1_concurrent_claim_for_same_thread_is_conflict_safe() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
runtime
@@ -1499,9 +1511,13 @@ mod tests {
#[tokio::test]
async fn stage1_concurrent_claims_respect_running_cap() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
let thread_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
@@ -1567,9 +1583,13 @@ mod tests {
#[tokio::test]
async fn claim_stage1_jobs_filters_by_age_idle_and_current_thread() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let now = Utc::now();
let fresh_at = now - Duration::hours(1);
@@ -1657,9 +1677,13 @@ mod tests {
#[tokio::test]
async fn claim_stage1_jobs_prefilters_threads_with_up_to_date_memory() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let now = Utc::now();
let eligible_newer_at = now - Duration::hours(13);
@@ -1756,9 +1780,13 @@ mod tests {
#[tokio::test]
async fn claim_stage1_jobs_skips_threads_with_disabled_memory_mode() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let now = Utc::now();
let eligible_at = now - Duration::hours(13);
@@ -1827,9 +1855,13 @@ mod tests {
#[tokio::test]
async fn clear_memory_data_clears_rows_and_preserves_thread_memory_modes() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let now = Utc::now() - Duration::hours(13);
let worker_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("worker id");
@@ -1936,9 +1968,13 @@ mod tests {
#[tokio::test]
async fn claim_stage1_jobs_enforces_global_running_cap() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let current_thread_id =
ThreadId::from_string(&Uuid::new_v4().to_string()).expect("current thread id");
@@ -2063,9 +2099,13 @@ WHERE kind = 'memory_stage1'
#[tokio::test]
async fn claim_stage1_jobs_processes_two_full_batches_across_startup_passes() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let current_thread_id =
ThreadId::from_string(&Uuid::new_v4().to_string()).expect("current thread id");
@@ -2150,9 +2190,13 @@ WHERE kind = 'memory_stage1'
#[tokio::test]
async fn stage1_output_cascades_on_thread_delete() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
@@ -2220,9 +2264,13 @@ WHERE kind = 'memory_stage1'
#[tokio::test]
async fn mark_stage1_job_succeeded_no_output_skips_phase2_when_output_was_already_absent() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
@@ -2295,9 +2343,13 @@ WHERE kind = 'memory_stage1'
#[tokio::test]
async fn mark_stage1_job_succeeded_no_output_enqueues_phase2_when_deleting_output() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
@@ -2420,9 +2472,13 @@ WHERE kind = 'memory_stage1'
#[tokio::test]
async fn stage1_retry_exhaustion_does_not_block_newer_watermark() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
@@ -2516,9 +2572,13 @@ WHERE kind = 'memory_stage1'
#[tokio::test]
async fn phase2_global_lock_respects_success_cooldown() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
@@ -2578,9 +2638,13 @@ WHERE kind = 'memory_stage1'
#[tokio::test]
async fn phase2_global_lock_can_be_claimed_after_retry_budget_is_exhausted() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
runtime
.enqueue_global_consolidation(/*input_watermark*/ 100)
@@ -2648,9 +2712,13 @@ WHERE kind = 'memory_stage1'
#[tokio::test]
async fn list_stage1_outputs_for_global_returns_latest_outputs() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
let thread_id_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
@@ -2751,9 +2819,13 @@ WHERE kind = 'memory_stage1'
#[tokio::test]
async fn list_stage1_outputs_for_global_skips_empty_payloads() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id_non_empty =
ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
@@ -2820,9 +2892,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn list_stage1_outputs_for_global_skips_polluted_threads() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id_enabled =
ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
@@ -2888,9 +2964,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn get_phase2_input_selection_returns_current_selected_rows() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id_a = stable_thread_id("00000000-0000-4000-8000-000000000001");
let thread_id_b = stable_thread_id("00000000-0000-4000-8000-000000000002");
@@ -3003,9 +3083,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn get_phase2_input_selection_excludes_polluted_previous_selection() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id_enabled =
ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
@@ -3096,9 +3180,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn mark_thread_memory_mode_polluted_enqueues_phase2_for_selected_threads() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
@@ -3185,9 +3273,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn get_phase2_input_selection_returns_regenerated_selected_rows() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
@@ -3304,9 +3396,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn get_phase2_input_selection_uses_current_ranking_after_refreshes() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id_a = stable_thread_id("00000000-0000-4000-8000-000000000001");
let thread_id_b = stable_thread_id("00000000-0000-4000-8000-000000000002");
@@ -3447,9 +3543,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn mark_global_phase2_job_succeeded_updates_selected_snapshot_timestamp() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
@@ -3597,9 +3697,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn mark_global_phase2_job_succeeded_only_marks_exact_selected_snapshots() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id");
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
@@ -3717,9 +3821,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn record_stage1_output_usage_updates_usage_metadata() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id a");
let thread_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id b");
@@ -3836,9 +3944,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn get_phase2_input_selection_prioritizes_usage_count_then_recent_usage() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let now = Utc::now();
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
@@ -3932,9 +4044,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn get_phase2_input_selection_excludes_stale_used_memories_but_keeps_fresh_never_used() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let now = Utc::now();
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
@@ -4028,9 +4144,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn get_phase2_input_selection_prefers_recent_thread_updates_over_recent_generation() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
let older_thread =
@@ -4114,9 +4234,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn prune_stage1_outputs_for_retention_prunes_stale_unselected_rows_only() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
let stale_unused =
@@ -4256,9 +4380,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn prune_stage1_outputs_for_retention_respects_batch_limit() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id");
let thread_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread a");
@@ -4334,9 +4462,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn mark_stage1_job_succeeded_enqueues_global_consolidation() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let thread_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id a");
let thread_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id b");
@@ -4429,9 +4561,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn phase2_global_lock_allows_only_one_fresh_runner() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
runtime
.enqueue_global_consolidation(/*input_watermark*/ 200)
@@ -4462,9 +4598,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn phase2_global_lock_creates_missing_job_row() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner a");
let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner b");
@@ -4512,9 +4652,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn phase2_global_lock_stale_lease_allows_takeover() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
runtime
.enqueue_global_consolidation(/*input_watermark*/ 300)
@@ -4587,9 +4731,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn enqueue_global_consolidation_keeps_phase2_input_watermark_monotonic() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
runtime
.enqueue_global_consolidation(/*input_watermark*/ 500)
@@ -4651,9 +4799,13 @@ VALUES (?, ?, ?, ?, ?)
#[tokio::test]
async fn phase2_failure_fallback_updates_unowned_running_job() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
runtime
.enqueue_global_consolidation(/*input_watermark*/ 400)
+14 -6
View File
@@ -130,9 +130,13 @@ mod tests {
#[tokio::test]
async fn remote_control_enrollment_round_trips_by_target_and_account() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
runtime
.upsert_remote_control_enrollment(&RemoteControlEnrollmentRecord {
@@ -207,9 +211,13 @@ mod tests {
#[tokio::test]
async fn delete_remote_control_enrollment_removes_only_matching_entry() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("initialize runtime");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("initialize runtime");
runtime
.upsert_remote_control_enrollment(&RemoteControlEnrollmentRecord {
+98 -42
View File
@@ -1156,9 +1156,13 @@ mod tests {
#[tokio::test]
async fn upsert_thread_keeps_creation_memory_mode_for_existing_rows() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("state db should initialize");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let thread_id =
ThreadId::from_string("00000000-0000-0000-0000-000000000123").expect("valid thread id");
let mut metadata = test_thread_metadata(&codex_home, thread_id, codex_home.clone());
@@ -1194,9 +1198,13 @@ mod tests {
#[tokio::test]
async fn list_threads_updated_after_returns_oldest_changes_first() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("state db should initialize");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let older_id =
ThreadId::from_string("00000000-0000-0000-0000-000000000001").expect("valid thread id");
let middle_id =
@@ -1278,9 +1286,13 @@ mod tests {
#[tokio::test]
async fn list_threads_filters_by_cwd() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("state db should initialize");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let first_id =
ThreadId::from_string("00000000-0000-0000-0000-000000000101").expect("valid thread id");
let second_id =
@@ -1349,9 +1361,13 @@ mod tests {
#[tokio::test]
async fn apply_rollout_items_restores_memory_mode_from_session_meta() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("state db should initialize");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let thread_id =
ThreadId::from_string("00000000-0000-0000-0000-000000000456").expect("valid thread id");
let metadata = test_thread_metadata(&codex_home, thread_id, codex_home.clone());
@@ -1406,9 +1422,13 @@ mod tests {
#[tokio::test]
async fn apply_rollout_items_preserves_existing_git_branch_and_fills_missing_git_fields() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("state db should initialize");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let thread_id =
ThreadId::from_string("00000000-0000-0000-0000-000000000457").expect("valid thread id");
let mut metadata = test_thread_metadata(&codex_home, thread_id, codex_home.clone());
@@ -1475,9 +1495,13 @@ mod tests {
#[tokio::test]
async fn upsert_thread_preserves_existing_git_fields_atomically() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("state db should initialize");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let thread_id =
ThreadId::from_string("00000000-0000-0000-0000-000000000458").expect("valid thread id");
let mut metadata = test_thread_metadata(&codex_home, thread_id, codex_home.clone());
@@ -1516,9 +1540,13 @@ mod tests {
#[tokio::test]
async fn update_thread_git_info_preserves_newer_non_git_metadata() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("state db should initialize");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let thread_id =
ThreadId::from_string("00000000-0000-0000-0000-000000000789").expect("valid thread id");
let metadata = test_thread_metadata(&codex_home, thread_id, codex_home.clone());
@@ -1576,9 +1604,13 @@ mod tests {
#[tokio::test]
async fn insert_thread_if_absent_preserves_existing_metadata() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("state db should initialize");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let thread_id =
ThreadId::from_string("00000000-0000-0000-0000-000000000791").expect("valid thread id");
@@ -1621,9 +1653,13 @@ mod tests {
#[tokio::test]
async fn update_thread_git_info_can_clear_fields() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("state db should initialize");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let thread_id =
ThreadId::from_string("00000000-0000-0000-0000-000000000790").expect("valid thread id");
let mut metadata = test_thread_metadata(&codex_home, thread_id, codex_home.clone());
@@ -1655,9 +1691,13 @@ mod tests {
#[tokio::test]
async fn touch_thread_updated_at_updates_only_updated_at() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("state db should initialize");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let thread_id =
ThreadId::from_string("00000000-0000-0000-0000-000000000791").expect("valid thread id");
let mut metadata = test_thread_metadata(&codex_home, thread_id, codex_home.clone());
@@ -1692,9 +1732,13 @@ mod tests {
#[tokio::test]
async fn thread_updated_at_uses_unique_epoch_millis_and_reads_legacy_seconds() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("state db should initialize");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let first_id =
ThreadId::from_string("00000000-0000-0000-0000-000000000901").expect("valid thread id");
let second_id =
@@ -1790,9 +1834,13 @@ mod tests {
#[tokio::test]
async fn apply_rollout_items_uses_override_updated_at_when_provided() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
.await
.expect("state db should initialize");
let runtime = StateRuntime::init(
codex_home.clone(),
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let thread_id =
ThreadId::from_string("00000000-0000-0000-0000-000000000792").expect("valid thread id");
let metadata = test_thread_metadata(&codex_home, thread_id, codex_home.clone());
@@ -1849,9 +1897,13 @@ mod tests {
#[tokio::test]
async fn thread_spawn_edges_track_directional_status() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home, "test-provider".to_string())
.await
.expect("state db should initialize");
let runtime = StateRuntime::init(
codex_home,
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let parent_thread_id =
ThreadId::from_string("00000000-0000-0000-0000-000000000900").expect("valid thread id");
let child_thread_id =
@@ -1945,9 +1997,13 @@ mod tests {
#[tokio::test]
async fn thread_spawn_children_without_status_filter_lists_all_statuses() {
let codex_home = unique_temp_dir();
let runtime = StateRuntime::init(codex_home, "test-provider".to_string())
.await
.expect("state db should initialize");
let runtime = StateRuntime::init(
codex_home,
"test-provider".to_string(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let parent_thread_id =
ThreadId::from_string("00000000-0000-0000-0000-000000000910").expect("valid thread id");
let open_child_thread_id =
+263
View File
@@ -0,0 +1,263 @@
use std::borrow::Cow;
use std::sync::Arc;
use std::time::Duration;
use crate::DB_FALLBACK_METRIC;
use crate::DB_INIT_DURATION_METRIC;
use crate::DB_INIT_METRIC;
/// Low-cardinality metrics sink used by the SQLite state runtime.
///
/// Implementations should ignore recording errors locally. Database operations
/// must never fail because telemetry delivery failed.
pub trait DbMetricsRecorder: Send + Sync + 'static {
/// Increment a counter metric by `inc` with low-cardinality tags.
fn counter(&self, name: &str, inc: i64, tags: &[(&str, &str)]);
/// Record an elapsed duration metric with low-cardinality tags.
fn record_duration(&self, name: &str, duration: Duration, tags: &[(&str, &str)]);
}
/// Shared recorder handle stored by rollout SQLite telemetry plumbing.
pub type DbMetricsRecorderHandle = Arc<dyn DbMetricsRecorder>;
#[derive(Clone, Copy)]
pub(crate) enum DbKind {
State,
Logs,
}
impl DbKind {
fn as_str(self) -> &'static str {
match self {
Self::State => "state",
Self::Logs => "logs",
}
}
}
pub(crate) fn record_init_result<T>(
metrics: Option<&dyn DbMetricsRecorder>,
db: DbKind,
phase: &'static str,
duration: Duration,
result: &anyhow::Result<T>,
) {
let outcome = DbOutcomeTags::from_result(result);
let tags = [
("status", outcome.status),
("phase", phase),
("db", db.as_str()),
("error", outcome.error),
];
record_counter(metrics, DB_INIT_METRIC, &tags);
record_duration(metrics, DB_INIT_DURATION_METRIC, duration, &tags);
}
pub fn record_fallback(
metrics: Option<&dyn DbMetricsRecorder>,
caller: &'static str,
reason: &'static str,
) {
let tags = [("caller", caller), ("reason", reason)];
record_counter(metrics, DB_FALLBACK_METRIC, &tags);
}
pub fn record_init_backfill_gate(
metrics: Option<&dyn DbMetricsRecorder>,
duration: Duration,
result: &anyhow::Result<()>,
) {
record_init_result(metrics, DbKind::State, "backfill_gate", duration, result);
}
pub(crate) fn classify_error(err: &anyhow::Error) -> &'static str {
for cause in err.chain() {
if let Some(sqlx_err) = cause.downcast_ref::<sqlx::Error>() {
return classify_sqlx_error(sqlx_err);
}
if cause
.downcast_ref::<sqlx::migrate::MigrateError>()
.is_some()
{
return "migration";
}
if cause.downcast_ref::<serde_json::Error>().is_some() {
return "serde";
}
if cause.downcast_ref::<std::io::Error>().is_some() {
return "io";
}
}
"unknown"
}
pub(crate) fn classify_sqlite_code(code: &str) -> &'static str {
// SQLite result codes are documented at https://www.sqlite.org/rescode.html.
// Extended codes preserve the primary code in the low byte.
let primary_code = code.parse::<i32>().ok().map(|code| code & 0xff);
match primary_code {
Some(5) => "busy",
Some(6) => "locked",
Some(8) => "readonly",
Some(10) => "io",
Some(11) => "corrupt",
Some(13) => "full",
Some(14) => "cantopen",
Some(19) => "constraint",
Some(17) => "schema",
_ => "unknown",
}
}
struct DbOutcomeTags {
status: &'static str,
error: &'static str,
}
impl DbOutcomeTags {
fn from_result<T>(result: &anyhow::Result<T>) -> Self {
match result {
Ok(_) => Self {
status: "success",
error: "none",
},
Err(err) => Self {
status: "failed",
error: classify_error(err),
},
}
}
}
fn classify_sqlx_error(err: &sqlx::Error) -> &'static str {
match err {
sqlx::Error::Database(database_error) => {
let code = database_error
.code()
.unwrap_or(Cow::Borrowed("none"))
.to_string();
classify_sqlite_code(code.as_str())
}
sqlx::Error::PoolTimedOut => "pool_timeout",
sqlx::Error::Io(_) => "io",
sqlx::Error::ColumnDecode { source, .. } if source.is::<serde_json::Error>() => "serde",
sqlx::Error::Decode(source) if source.is::<serde_json::Error>() => "serde",
_ => "unknown",
}
}
fn record_counter(metrics: Option<&dyn DbMetricsRecorder>, name: &str, tags: &[(&str, &str)]) {
if let Some(metrics) = metrics {
metrics.counter(name, /*inc*/ 1, tags);
}
}
fn record_duration(
metrics: Option<&dyn DbMetricsRecorder>,
name: &str,
duration: Duration,
tags: &[(&str, &str)],
) {
if let Some(metrics) = metrics {
metrics.record_duration(name, duration, tags);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DB_FALLBACK_METRIC;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
use std::sync::Mutex;
#[derive(Default)]
struct TestMetrics {
events: Mutex<Vec<MetricEvent>>,
}
#[derive(Debug, Eq, PartialEq)]
struct MetricEvent {
name: String,
tags: BTreeMap<String, String>,
}
impl TestMetrics {
fn events(&self) -> Vec<MetricEvent> {
self.events
.lock()
.expect("metrics lock")
.iter()
.map(|event| MetricEvent {
name: event.name.clone(),
tags: event.tags.clone(),
})
.collect()
}
}
impl DbMetricsRecorder for TestMetrics {
fn counter(&self, name: &str, _inc: i64, tags: &[(&str, &str)]) {
self.events.lock().expect("metrics lock").push(MetricEvent {
name: name.to_string(),
tags: tags_to_map(tags),
});
}
fn record_duration(&self, _name: &str, _duration: Duration, _tags: &[(&str, &str)]) {}
}
fn tags_to_map(tags: &[(&str, &str)]) -> BTreeMap<String, String> {
tags.iter()
.map(|(key, value)| ((*key).to_string(), (*value).to_string()))
.collect()
}
#[test]
fn classifies_sqlite_primary_codes() {
assert_eq!(classify_sqlite_code("5"), "busy");
assert_eq!(classify_sqlite_code("6"), "locked");
assert_eq!(classify_sqlite_code("14"), "cantopen");
assert_eq!(classify_sqlite_code("2067"), "constraint");
}
#[test]
fn classifies_non_sqlite_errors() {
let io_error =
anyhow::Error::new(std::io::Error::new(std::io::ErrorKind::NotFound, "missing"));
assert_eq!(classify_error(&io_error), "io");
let serde_error =
anyhow::Error::new(serde_json::from_str::<serde_json::Value>("not-json").unwrap_err());
assert_eq!(classify_error(&serde_error), "serde");
let unknown_error = anyhow::anyhow!("plain failure");
assert_eq!(classify_error(&unknown_error), "unknown");
}
#[test]
fn classifies_sqlx_pool_timeout() {
let err = anyhow::Error::new(sqlx::Error::PoolTimedOut);
assert_eq!(classify_error(&err), "pool_timeout");
}
#[test]
fn records_fallback_metric_with_reason() {
let metrics = TestMetrics::default();
record_fallback(Some(&metrics), "list_threads", "db_error");
assert_eq!(
metrics.events(),
vec![MetricEvent {
name: DB_FALLBACK_METRIC.to_string(),
tags: BTreeMap::from([
("caller".to_string(), "list_threads".to_string()),
("reason".to_string(), "db_error".to_string()),
]),
}]
);
}
}
+5 -3
View File
@@ -42,6 +42,7 @@ use codex_core_api::RealtimeConfig;
use codex_core_api::SessionPickerViewMode;
use codex_core_api::SessionSource;
use codex_core_api::ShellEnvironmentPolicy;
use codex_core_api::StateDbAccess;
use codex_core_api::TerminalResizeReflowConfig;
use codex_core_api::ThreadManager;
use codex_core_api::ThreadStoreConfig;
@@ -104,7 +105,8 @@ async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
};
let config = new_config(args.model, arg0_paths)?;
let state_db = init_state_db(&config).await;
let state_db = init_state_db(&config, /*metrics*/ None).await;
let state_db_access = StateDbAccess::new(state_db.clone());
let auth_manager =
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await;
@@ -112,7 +114,7 @@ async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
config.codex_self_exe.clone(),
config.codex_linux_sandbox_exe.clone(),
)?;
let thread_store = thread_store_from_config(&config, state_db.clone());
let thread_store = thread_store_from_config(&config, state_db_access.clone());
let environment_manager = Arc::new(
EnvironmentManager::from_codex_home(config.codex_home.clone(), local_runtime_paths).await?,
);
@@ -124,7 +126,7 @@ async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
environment_manager,
/*analytics_events_client*/ None,
Arc::clone(&thread_store),
state_db,
state_db_access,
installation_id,
/*attestation_provider*/ None,
);
@@ -13,11 +13,11 @@ pub(super) async fn archive_thread(
params: ArchiveThreadParams,
) -> ThreadStoreResult<()> {
let thread_id = params.thread_id;
let state_db_ctx = store.state_db().await;
let state_db_access = store.state_db_access();
let rollout_path = find_thread_path_by_id_str(
store.config.codex_home.as_path(),
&thread_id.to_string(),
state_db_ctx.as_deref(),
&state_db_access,
)
.await
.map_err(|err| ThreadStoreError::InvalidRequest {
@@ -52,7 +52,7 @@ pub(super) async fn archive_thread(
}
})?;
if let Some(ctx) = state_db_ctx {
if let Some(ctx) = state_db_access.state_db() {
let _ = ctx
.mark_archived(thread_id, archived_path.as_path(), Utc::now())
.await;
@@ -81,7 +81,10 @@ mod tests {
#[tokio::test]
async fn archive_thread_moves_rollout_to_archived_collection() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = Uuid::from_u128(201);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
let active_path =
@@ -134,10 +137,14 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
home.path().to_path_buf(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
runtime
.mark_backfill_complete(/*last_watermark*/ None)
.await
+26 -13
View File
@@ -39,7 +39,7 @@ pub(super) async fn list_threads(
SortDirection::Asc => codex_rollout::SortDirection::Asc,
SortDirection::Desc => codex_rollout::SortDirection::Desc,
};
let state_db = store.state_db().await;
let state_db_access = store.state_db_access();
let rollout_config = RolloutConfig {
codex_home: store.config.codex_home.clone(),
sqlite_home: store.config.sqlite_home.clone(),
@@ -48,7 +48,7 @@ pub(super) async fn list_threads(
generate_memories: false,
};
let page = list_rollout_threads(
state_db,
state_db_access.clone(),
&rollout_config,
store.config.default_model_provider_id.as_str(),
&params,
@@ -80,7 +80,7 @@ pub(super) async fn list_threads(
.map(|thread| thread.thread_id)
.collect::<HashSet<_>>();
let mut names = HashMap::<ThreadId, String>::with_capacity(thread_ids.len());
if let Some(state_db_ctx) = store.state_db().await {
if let Some(state_db_ctx) = state_db_access.state_db() {
for &thread_id in &thread_ids {
let Ok(Some(metadata)) = state_db_ctx.get_thread(thread_id).await else {
continue;
@@ -108,7 +108,7 @@ pub(super) async fn list_threads(
}
async fn list_rollout_threads(
state_db: Option<codex_rollout::StateDbHandle>,
state_db_ctx: codex_rollout::StateDbAccess,
config: &RolloutConfig,
default_model_provider_id: &str,
params: &ListThreadsParams,
@@ -118,7 +118,7 @@ async fn list_rollout_threads(
) -> ThreadStoreResult<codex_rollout::ThreadsPage> {
let page = if params.use_state_db_only && params.archived {
RolloutRecorder::list_archived_threads_from_state_db(
state_db,
state_db_ctx,
config,
params.page_size,
cursor,
@@ -133,7 +133,7 @@ async fn list_rollout_threads(
.await
} else if params.use_state_db_only {
RolloutRecorder::list_threads_from_state_db(
state_db,
state_db_ctx,
config,
params.page_size,
cursor,
@@ -148,7 +148,7 @@ async fn list_rollout_threads(
.await
} else if params.archived {
RolloutRecorder::list_archived_threads(
state_db,
state_db_ctx,
config,
params.page_size,
cursor,
@@ -163,7 +163,7 @@ async fn list_rollout_threads(
.await
} else {
RolloutRecorder::list_threads(
state_db,
state_db_ctx,
config,
params.page_size,
cursor,
@@ -203,7 +203,10 @@ mod tests {
#[tokio::test]
async fn list_threads_uses_default_provider_when_rollout_omits_provider() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
write_session_file_with(
home.path(),
home.path().join("sessions/2025/01/03"),
@@ -246,10 +249,14 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
home.path().to_path_buf(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
runtime
.mark_backfill_complete(/*last_watermark*/ None)
.await
@@ -303,7 +310,10 @@ mod tests {
#[tokio::test]
async fn list_threads_selects_active_or_archived_collection() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let active_uuid = Uuid::from_u128(105);
let archived_uuid = Uuid::from_u128(106);
write_session_file(home.path(), "2025-01-03T12-00-00", active_uuid)
@@ -372,7 +382,7 @@ mod tests {
async fn list_threads_returns_local_rollout_summary() {
let home = TempDir::new().expect("temp dir");
let config = test_config(home.path());
let store = LocalThreadStore::new(config, /*state_db*/ None);
let store = LocalThreadStore::new(config, codex_rollout::StateDbAccess::none());
let uuid = Uuid::from_u128(101);
let path =
write_session_file(home.path(), "2025-01-03T12-00-00", uuid).expect("session file");
@@ -411,7 +421,10 @@ mod tests {
#[tokio::test]
async fn list_threads_rejects_invalid_cursor() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let err = store
.list_threads(ListThreadsParams {
+51 -15
View File
@@ -13,6 +13,7 @@ mod test_support;
use async_trait::async_trait;
use codex_protocol::ThreadId;
use codex_rollout::RolloutRecorder;
use codex_rollout::StateDbAccess;
use codex_rollout::StateDbHandle;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
@@ -41,7 +42,7 @@ use crate::UpdateThreadMetadataParams;
pub struct LocalThreadStore {
pub(super) config: LocalThreadStoreConfig,
live_recorders: Arc<Mutex<HashMap<ThreadId, RolloutRecorder>>>,
state_db: Option<StateDbHandle>,
state_db: StateDbAccess,
}
/// Process-scoped configuration for local thread storage.
@@ -75,8 +76,8 @@ impl std::fmt::Debug for LocalThreadStore {
}
impl LocalThreadStore {
/// Create a local store using an already initialized state DB handle.
pub fn new(config: LocalThreadStoreConfig, state_db: Option<StateDbHandle>) -> Self {
/// Create a local store with explicit SQLite access.
pub fn new(config: LocalThreadStoreConfig, state_db: StateDbAccess) -> Self {
Self {
config,
live_recorders: Arc::new(Mutex::new(HashMap::new())),
@@ -86,6 +87,10 @@ impl LocalThreadStore {
/// Return the state DB handle used by local rollout writers.
pub async fn state_db(&self) -> Option<StateDbHandle> {
self.state_db.state_db()
}
pub(super) fn state_db_access(&self) -> StateDbAccess {
self.state_db.clone()
}
@@ -289,7 +294,10 @@ mod tests {
#[tokio::test]
async fn live_writer_lifecycle_writes_and_closes() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let thread_id = ThreadId::default();
store
@@ -338,7 +346,10 @@ mod tests {
#[tokio::test]
async fn create_thread_rejects_missing_cwd() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let thread_id = ThreadId::default();
let mut params = create_thread_params(thread_id);
params.metadata.cwd = None;
@@ -358,7 +369,10 @@ mod tests {
#[tokio::test]
async fn discard_thread_drops_unmaterialized_live_writer() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let thread_id = ThreadId::default();
store
@@ -397,7 +411,8 @@ mod tests {
let config = test_config(home.path());
let thread_id = ThreadId::default();
let first_store = LocalThreadStore::new(config.clone(), /*state_db*/ None);
let first_store =
LocalThreadStore::new(config.clone(), codex_rollout::StateDbAccess::none());
first_store
.create_thread(create_thread_params(thread_id))
.await
@@ -426,7 +441,7 @@ mod tests {
.await
.expect("shutdown initial writer");
let resumed_store = LocalThreadStore::new(config, /*state_db*/ None);
let resumed_store = LocalThreadStore::new(config, codex_rollout::StateDbAccess::none());
resumed_store
.resume_thread(ResumeThreadParams {
thread_id,
@@ -457,7 +472,10 @@ mod tests {
#[tokio::test]
async fn create_thread_rejects_duplicate_live_writer() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let thread_id = ThreadId::default();
store
@@ -477,7 +495,10 @@ mod tests {
#[tokio::test]
async fn resume_thread_rejects_duplicate_live_writer() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let thread_id = ThreadId::default();
store
@@ -506,7 +527,10 @@ mod tests {
#[tokio::test]
async fn resume_thread_rejects_missing_cwd() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = uuid::Uuid::from_u128(407);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
let rollout_path =
@@ -535,7 +559,10 @@ mod tests {
async fn load_history_uses_live_writer_rollout_path() {
let home = TempDir::new().expect("temp dir");
let external_home = TempDir::new().expect("external temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = uuid::Uuid::from_u128(404);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
let rollout_path = write_session_file(external_home.path(), "2025-01-04T10-00-00", uuid)
@@ -584,7 +611,10 @@ mod tests {
async fn read_thread_uses_live_writer_rollout_path_for_external_resume() {
let home = TempDir::new().expect("temp dir");
let external_home = TempDir::new().expect("external temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = uuid::Uuid::from_u128(406);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
let rollout_path = write_session_file(external_home.path(), "2025-01-04T11-00-00", uuid)
@@ -623,7 +653,10 @@ mod tests {
#[tokio::test]
async fn load_history_uses_live_writer_rollout_path_for_archived_source() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = uuid::Uuid::from_u128(405);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
let rollout_path = write_archived_session_file(home.path(), "2025-01-04T10-30-00", uuid)
@@ -691,7 +724,10 @@ mod tests {
#[tokio::test]
async fn read_thread_by_rollout_path_includes_history() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let thread_id = ThreadId::default();
store
+81 -21
View File
@@ -176,12 +176,12 @@ async fn resolve_rollout_path(
return Ok(Some(path));
}
let state_db_ctx = store.state_db().await;
let state_db_access = store.state_db_access();
if include_archived {
match find_thread_path_by_id_str(
store.config.codex_home.as_path(),
&thread_id.to_string(),
state_db_ctx.as_deref(),
&state_db_access,
)
.await
.map_err(|err| ThreadStoreError::InvalidRequest {
@@ -191,7 +191,7 @@ async fn resolve_rollout_path(
None => find_archived_thread_path_by_id_str(
store.config.codex_home.as_path(),
&thread_id.to_string(),
state_db_ctx.as_deref(),
&state_db_access,
)
.await
.map_err(|err| ThreadStoreError::InvalidRequest {
@@ -202,7 +202,7 @@ async fn resolve_rollout_path(
find_thread_path_by_id_str(
store.config.codex_home.as_path(),
&thread_id.to_string(),
state_db_ctx.as_deref(),
&state_db_access,
)
.await
.map_err(|err| ThreadStoreError::InvalidRequest {
@@ -423,7 +423,10 @@ mod tests {
#[tokio::test]
async fn read_thread_returns_active_rollout_summary() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = Uuid::from_u128(205);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
let active_path =
@@ -451,7 +454,10 @@ mod tests {
#[tokio::test]
async fn read_thread_returns_rollout_path_summary() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = Uuid::from_u128(211);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
let active_path =
@@ -489,10 +495,14 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
let mut builder = ThreadMetadataBuilder::new(
thread_id,
active_path.clone(),
@@ -530,7 +540,10 @@ mod tests {
#[tokio::test]
async fn read_thread_returns_archived_rollout_when_requested() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = Uuid::from_u128(207);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
let archived_path = write_archived_session_file(home.path(), "2025-01-03T12-00-00", uuid)
@@ -571,7 +584,10 @@ mod tests {
#[tokio::test]
async fn read_thread_prefers_active_rollout_over_archived() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = Uuid::from_u128(208);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
let active_path =
@@ -596,7 +612,10 @@ mod tests {
#[tokio::test]
async fn read_thread_returns_forked_from_id() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = Uuid::from_u128(209);
let parent_uuid = Uuid::from_u128(210);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
@@ -636,10 +655,14 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
let mut builder =
ThreadMetadataBuilder::new(thread_id, rollout_path, Utc::now(), SessionSource::Cli);
builder.model_provider = Some(config.default_model_provider_id.clone());
@@ -672,10 +695,14 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
let uuid = Uuid::from_u128(224);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
let day_dir = home.path().join("sessions/2025/01/03");
@@ -744,7 +771,10 @@ mod tests {
#[tokio::test]
async fn read_thread_uses_legacy_thread_name_when_sqlite_title_is_missing() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = Uuid::from_u128(213);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
write_session_file(home.path(), "2025-01-03T12-00-00", uuid).expect("session file");
@@ -792,10 +822,14 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
let mut builder = ThreadMetadataBuilder::new(
thread_id,
rollout_path.clone(),
@@ -846,10 +880,14 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
let mut builder = ThreadMetadataBuilder::new(
thread_id,
stale_path.clone(),
@@ -897,10 +935,14 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
let mut builder =
ThreadMetadataBuilder::new(thread_id, stale_path, Utc::now(), SessionSource::Cli);
builder.model_provider = Some("wrong-sqlite-provider".to_string());
@@ -932,7 +974,10 @@ mod tests {
#[tokio::test]
async fn read_thread_uses_session_meta_for_rollout_without_user_preview_or_sqlite_metadata() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = Uuid::from_u128(218);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
let day_dir = home.path().join("sessions/2025/01/03");
@@ -995,10 +1040,14 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
let mut builder = ThreadMetadataBuilder::new(
thread_id,
rollout_path.clone(),
@@ -1053,12 +1102,16 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let mut builder =
ThreadMetadataBuilder::new(thread_id, rollout_path, Utc::now(), SessionSource::Cli);
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
builder.archived_at = Some(Utc::now());
let mut metadata = builder.build(config.default_model_provider_id.as_str());
metadata.first_user_message = Some("Archived SQLite preview".to_string());
@@ -1108,10 +1161,14 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
let mut builder = ThreadMetadataBuilder::new(
thread_id,
archived_path.clone(),
@@ -1147,7 +1204,10 @@ mod tests {
#[tokio::test]
async fn read_thread_fails_without_rollout() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = Uuid::from_u128(206);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
@@ -17,11 +17,11 @@ pub(super) async fn unarchive_thread(
params: ArchiveThreadParams,
) -> ThreadStoreResult<StoredThread> {
let thread_id = params.thread_id;
let state_db_ctx = store.state_db().await;
let state_db_access = store.state_db_access();
let archived_path = find_archived_thread_path_by_id_str(
store.config.codex_home.as_path(),
&thread_id.to_string(),
state_db_ctx.as_deref(),
&state_db_access,
)
.await
.map_err(|err| ThreadStoreError::InvalidRequest {
@@ -73,7 +73,7 @@ pub(super) async fn unarchive_thread(
message: format!("failed to update unarchived thread timestamp: {err}"),
})?;
if let Some(ctx) = state_db_ctx {
if let Some(ctx) = state_db_access.state_db() {
let _ = ctx
.mark_unarchived(thread_id, restored_path.as_path())
.await;
@@ -118,7 +118,10 @@ mod tests {
#[tokio::test]
async fn unarchive_thread_restores_rollout_and_returns_updated_thread() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = Uuid::from_u128(203);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
let archived_path = write_archived_session_file(home.path(), "2025-01-03T13-00-00", uuid)
@@ -156,10 +159,14 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
home.path().to_path_buf(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
runtime
.mark_backfill_complete(/*last_watermark*/ None)
.await
@@ -310,11 +310,11 @@ async fn resolve_rollout_path(
return Ok(ResolvedRolloutPath { path, archived });
}
let state_db_ctx = store.state_db().await;
let state_db_access = store.state_db_access();
let active_path = find_thread_path_by_id_str(
store.config.codex_home.as_path(),
&thread_id.to_string(),
state_db_ctx.as_deref(),
&state_db_access,
)
.await
.map_err(|err| ThreadStoreError::InvalidRequest {
@@ -334,7 +334,7 @@ async fn resolve_rollout_path(
find_archived_thread_path_by_id_str(
store.config.codex_home.as_path(),
&thread_id.to_string(),
state_db_ctx.as_deref(),
&state_db_access,
)
.await
.map_err(|err| ThreadStoreError::InvalidRequest {
@@ -376,7 +376,10 @@ mod tests {
#[tokio::test]
async fn update_thread_metadata_sets_name_on_active_rollout_and_indexes_name() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = Uuid::from_u128(301);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
write_session_file(home.path(), "2025-01-03T14-00-00", uuid).expect("session file");
@@ -411,10 +414,14 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
home.path().to_path_buf(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
let thread = store
.update_thread_metadata(UpdateThreadMetadataParams {
@@ -451,10 +458,14 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
store
.update_thread_metadata(UpdateThreadMetadataParams {
@@ -513,7 +524,10 @@ mod tests {
async fn update_thread_metadata_uses_live_rollout_path_for_external_resume() {
let home = TempDir::new().expect("temp dir");
let external_home = TempDir::new().expect("external temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = Uuid::from_u128(307);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
let path = write_session_file(external_home.path(), "2025-01-03T14-45-00", uuid)
@@ -557,10 +571,11 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config, Some(runtime));
let store = LocalThreadStore::new(config, codex_rollout::StateDbAccess::new(Some(runtime)));
let uuid = Uuid::from_u128(309);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
write_session_file(home.path(), "2025-01-03T17-00-00", uuid).expect("session file");
@@ -600,10 +615,11 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config, Some(runtime));
let store = LocalThreadStore::new(config, codex_rollout::StateDbAccess::new(Some(runtime)));
let uuid = Uuid::from_u128(310);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
write_session_file(home.path(), "2025-01-03T17-30-00", uuid).expect("session file");
@@ -658,10 +674,14 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
let uuid = Uuid::from_u128(311);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
let path =
@@ -825,7 +845,10 @@ mod tests {
#[tokio::test]
async fn update_thread_metadata_rejects_mismatched_session_meta_id() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let filename_uuid = Uuid::from_u128(303);
let metadata_uuid = Uuid::from_u128(304);
let thread_id = ThreadId::from_string(&filename_uuid.to_string()).expect("valid thread id");
@@ -857,7 +880,10 @@ mod tests {
#[tokio::test]
async fn update_thread_metadata_rejects_multi_field_patch_without_partial_write() {
let home = TempDir::new().expect("temp dir");
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
let store = LocalThreadStore::new(
test_config(home.path()),
codex_rollout::StateDbAccess::none(),
);
let uuid = Uuid::from_u128(305);
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
let path =
@@ -899,10 +925,14 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
home.path().to_path_buf(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
runtime
.mark_backfill_complete(/*last_watermark*/ None)
.await
@@ -962,10 +992,14 @@ mod tests {
let runtime = codex_state::StateRuntime::init(
home.path().to_path_buf(),
config.default_model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
let store = LocalThreadStore::new(
config.clone(),
codex_rollout::StateDbAccess::new(Some(runtime.clone())),
);
runtime
.mark_backfill_complete(/*last_watermark*/ None)
.await
+4 -4
View File
@@ -145,7 +145,7 @@ use codex_protocol::openai_models::ModelUpgrade;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
#[cfg(target_os = "windows")]
use codex_protocol::permissions::FileSystemSandboxKind;
use codex_rollout::StateDbHandle;
use codex_rollout::StateDbAccess;
use codex_terminal_detection::user_agent;
use codex_utils_absolute_path::AbsolutePathBuf;
use color_eyre::eyre::Result;
@@ -437,7 +437,7 @@ pub(crate) struct App {
workspace_command_runner: Option<WorkspaceCommandRunner>,
/// Config is stored here so we can recreate ChatWidgets as needed.
pub(crate) config: Config,
pub(crate) state_db: Option<StateDbHandle>,
pub(crate) state_db_access: StateDbAccess,
pub(crate) active_profile: Option<String>,
cli_kv_overrides: Vec<(String, TomlValue)>,
harness_overrides: ConfigOverrides,
@@ -616,7 +616,7 @@ impl App {
should_prompt_windows_sandbox_nux_at_startup: bool,
remote_app_server_url: Option<String>,
remote_app_server_auth_token: Option<String>,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
environment_manager: Arc<EnvironmentManager>,
) -> Result<AppExitInfo> {
use tokio_stream::StreamExt;
@@ -872,7 +872,7 @@ See the Codex keymap documentation for supported actions and examples."
chat_widget,
workspace_command_runner: Some(workspace_command_runner),
config,
state_db,
state_db_access,
active_profile,
cli_kv_overrides,
harness_overrides,
+1 -1
View File
@@ -64,7 +64,7 @@ impl App {
},
None => crate::AppServerTarget::Embedded,
},
self.state_db.clone(),
self.state_db_access.clone(),
self.environment_manager.clone(),
)
.await
+1 -1
View File
@@ -638,7 +638,7 @@ impl App {
} else {
match crate::session_resume::resolve_cwd_for_resume_or_fork(
tui,
self.state_db.as_deref(),
self.state_db_access.as_deref(),
&current_cwd,
target_session.thread_id,
target_session.path.as_deref(),
+1 -1
View File
@@ -21,7 +21,7 @@ pub(super) async fn make_test_app() -> App {
chat_widget,
workspace_command_runner: None,
config,
state_db: None,
state_db_access: StateDbAccess::none(),
active_profile: None,
cli_kv_overrides: Vec::new(),
harness_overrides: ConfigOverrides::default(),
+3 -2
View File
@@ -1656,6 +1656,7 @@ fn update_memory_settings_updates_current_thread_memory_mode() -> Result<()> {
let state_db = codex_state::StateRuntime::init(
codex_home.path().to_path_buf(),
app.config.model_provider_id.clone(),
/*metrics*/ None,
)
.await
.expect("state db should initialize");
@@ -3867,7 +3868,7 @@ async fn make_test_app() -> App {
chat_widget,
workspace_command_runner: None,
config,
state_db: None,
state_db_access: StateDbAccess::none(),
active_profile: None,
cli_kv_overrides: Vec::new(),
harness_overrides: ConfigOverrides::default(),
@@ -3930,7 +3931,7 @@ async fn make_test_app_with_channels() -> (
chat_widget,
workspace_command_runner: None,
config,
state_db: None,
state_db_access: StateDbAccess::none(),
active_profile: None,
cli_kv_overrides: Vec::new(),
harness_overrides: ConfigOverrides::default(),
+6 -2
View File
@@ -916,8 +916,12 @@ impl App {
session.model_provider_id = notification.thread.model_provider.clone();
session.cwd = notification.thread.cwd.clone();
let rollout_path = notification.thread.path.clone();
if let Some(model) =
read_session_model(self.state_db.as_deref(), thread_id, rollout_path.as_deref()).await
if let Some(model) = read_session_model(
self.state_db_access.as_deref(),
thread_id,
rollout_path.as_deref(),
)
.await
{
session.model = model;
} else if rollout_path.is_some() {
+6 -2
View File
@@ -85,8 +85,12 @@ impl App {
session.active_permission_profile = active_permission_profile;
session.instruction_source_paths = Vec::new();
session.rollout_path = thread.path.clone();
if let Some(model) =
read_session_model(self.state_db.as_deref(), thread_id, thread.path.as_deref()).await
if let Some(model) = read_session_model(
self.state_db_access.as_deref(),
thread_id,
thread.path.as_deref(),
)
.await
{
session.model = model;
} else if thread.path.is_some() {
+61 -49
View File
@@ -42,13 +42,14 @@ use codex_config::format_config_error_with_source;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::ExecServerRuntimePaths;
use codex_login::AuthConfig;
use codex_login::default_client::originator;
use codex_login::default_client::set_default_client_residency_requirement;
use codex_login::enforce_login_restrictions;
use codex_protocol::ThreadId;
use codex_protocol::config_types::AltScreenMode;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_rollout::StateDbHandle;
use codex_rollout::StateDbAccess;
use codex_rollout::state_db;
use codex_state::log_db;
use codex_terminal_detection::terminal_info;
@@ -273,7 +274,7 @@ async fn start_embedded_app_server(
cloud_requirements: CloudRequirementsLoader,
feedback: codex_feedback::CodexFeedback,
log_db: Option<log_db::LogDbLayer>,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
environment_manager: Arc<EnvironmentManager>,
) -> color_eyre::Result<InProcessAppServerClient> {
start_embedded_app_server_with(
@@ -284,7 +285,7 @@ async fn start_embedded_app_server(
cloud_requirements,
feedback,
log_db,
state_db,
state_db_access,
environment_manager,
InProcessAppServerClient::start,
)
@@ -402,7 +403,7 @@ async fn start_app_server(
cloud_requirements: CloudRequirementsLoader,
feedback: codex_feedback::CodexFeedback,
log_db: Option<log_db::LogDbLayer>,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
environment_manager: Arc<EnvironmentManager>,
) -> color_eyre::Result<AppServerClient> {
match target {
@@ -414,7 +415,7 @@ async fn start_app_server(
cloud_requirements,
feedback,
log_db,
state_db,
state_db_access,
environment_manager,
)
.await
@@ -429,7 +430,7 @@ async fn start_app_server(
pub(crate) async fn start_app_server_for_picker(
config: &Config,
target: &AppServerTarget,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
environment_manager: Arc<EnvironmentManager>,
) -> color_eyre::Result<AppServerSession> {
let app_server = start_app_server(
@@ -441,7 +442,7 @@ pub(crate) async fn start_app_server_for_picker(
CloudRequirementsLoader::default(),
codex_feedback::CodexFeedback::new(),
/*log_db*/ None,
state_db,
state_db_access,
environment_manager,
)
.await?;
@@ -452,11 +453,11 @@ pub(crate) async fn start_app_server_for_picker(
pub(crate) async fn start_embedded_app_server_for_picker(
config: &Config,
) -> color_eyre::Result<AppServerSession> {
let state_db = state_db::init(config).await;
let state_db = state_db::init(config, /*metrics*/ None).await;
start_app_server_for_picker(
config,
&AppServerTarget::Embedded,
state_db,
StateDbAccess::new(state_db),
Arc::new(EnvironmentManager::default_for_tests()),
)
.await
@@ -471,7 +472,7 @@ async fn start_embedded_app_server_with<F, Fut>(
cloud_requirements: CloudRequirementsLoader,
feedback: codex_feedback::CodexFeedback,
log_db: Option<log_db::LogDbLayer>,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
environment_manager: Arc<EnvironmentManager>,
start_client: F,
) -> color_eyre::Result<InProcessAppServerClient>
@@ -497,7 +498,7 @@ where
cloud_requirements,
feedback,
log_db,
state_db,
state_db_access,
environment_manager,
config_warnings,
session_source: serde_json::from_value(serde_json::json!("cli"))
@@ -887,10 +888,45 @@ pub async fn run_main(
)
.await;
let state_db = match &app_server_target {
AppServerTarget::Embedded => state_db::init(&config).await,
AppServerTarget::Remote { .. } => state_db::get_state_db(&config).await,
let otel_originator = originator().value;
let otel = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
crate::legacy_core::otel_init::build_provider(
&config,
env!("CARGO_PKG_VERSION"),
/*service_name_override*/ None,
/*default_analytics_enabled*/ true,
)
})) {
Ok(Ok(otel)) => otel,
Ok(Err(e)) => {
#[allow(clippy::print_stderr)]
{
eprintln!("Could not create otel exporter: {e}");
}
None
}
Err(_) => {
#[allow(clippy::print_stderr)]
{
eprintln!("Could not create otel exporter: panicked during initialization");
}
None
}
};
crate::legacy_core::otel_init::record_process_start(otel.as_ref(), otel_originator.as_str());
let state_db_metrics = crate::legacy_core::otel_init::sqlite_metrics_recorder(
otel.as_ref(),
otel_originator.as_str(),
);
let state_db = match &app_server_target {
AppServerTarget::Embedded => state_db::init(&config, state_db_metrics.clone()).await,
AppServerTarget::Remote { .. } => {
state_db::get_state_db(&config, state_db_metrics.clone()).await
}
};
let state_db_access =
StateDbAccess::new(state_db.clone()).with_standalone_metrics(state_db_metrics.clone());
let effective_toml = config.config_layer_stack.effective_config();
match effective_toml.try_into() {
@@ -898,7 +934,7 @@ pub async fn run_main(
match crate::legacy_core::personality_migration::maybe_migrate_personality(
&config.codex_home,
&config_toml,
state_db.clone(),
state_db_access.clone(),
)
.await
{
@@ -1029,31 +1065,6 @@ pub async fn run_main(
ensure_oss_provider_ready(provider_id, &config).await?;
}
let otel = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
crate::legacy_core::otel_init::build_provider(
&config,
env!("CARGO_PKG_VERSION"),
/*service_name_override*/ None,
/*default_analytics_enabled*/ true,
)
})) {
Ok(Ok(otel)) => otel,
Ok(Err(e)) => {
#[allow(clippy::print_stderr)]
{
eprintln!("Could not create otel exporter: {e}");
}
None
}
Err(_) => {
#[allow(clippy::print_stderr)]
{
eprintln!("Could not create otel exporter: panicked during initialization");
}
None
}
};
let otel_logger_layer = otel.as_ref().and_then(|o| o.logger_layer());
let otel_tracing_layer = otel.as_ref().and_then(|o| o.tracing_layer());
@@ -1084,7 +1095,7 @@ pub async fn run_main(
cloud_requirements,
feedback,
log_db,
state_db,
state_db_access,
remote_url,
remote_auth_token,
environment_manager,
@@ -1106,7 +1117,7 @@ async fn run_ratatui_app(
mut cloud_requirements: CloudRequirementsLoader,
feedback: codex_feedback::CodexFeedback,
log_db: Option<log_db::LogDbLayer>,
state_db: Option<StateDbHandle>,
state_db_access: StateDbAccess,
remote_url: Option<String>,
remote_auth_token: Option<String>,
environment_manager: Arc<EnvironmentManager>,
@@ -1166,7 +1177,7 @@ async fn run_ratatui_app(
cloud_requirements.clone(),
feedback.clone(),
log_db.clone(),
state_db.clone(),
state_db_access.clone(),
environment_manager.clone(),
)
.await
@@ -1414,7 +1425,7 @@ async fn run_ratatui_app(
} else {
match resolve_cwd_for_resume_or_fork(
&mut tui,
state_db.as_deref(),
state_db_access.as_deref(),
&current_cwd,
target_session.thread_id,
target_session.path.as_deref(),
@@ -1505,7 +1516,7 @@ async fn run_ratatui_app(
cloud_requirements.clone(),
feedback.clone(),
log_db.clone(),
state_db.clone(),
state_db_access.clone(),
environment_manager.clone(),
)
.await
@@ -1536,7 +1547,7 @@ async fn run_ratatui_app(
should_prompt_windows_sandbox_nux_at_startup,
remote_url,
remote_auth_token,
state_db,
state_db_access,
environment_manager,
)
.await;
@@ -1741,7 +1752,7 @@ mod tests {
async fn start_test_embedded_app_server(
config: Config,
) -> color_eyre::Result<InProcessAppServerClient> {
let state_db = state_db::init(&config).await;
let state_db = state_db::init(&config, /*metrics*/ None).await;
start_embedded_app_server(
Arg0DispatchPaths::default(),
config,
@@ -1750,7 +1761,7 @@ mod tests {
CloudRequirementsLoader::default(),
codex_feedback::CodexFeedback::new(),
/*log_db*/ None,
state_db,
StateDbAccess::new(state_db),
Arc::new(EnvironmentManager::default_for_tests()),
)
.await
@@ -2214,6 +2225,7 @@ mod tests {
let state_runtime = codex_state::StateRuntime::init(
config.codex_home.to_path_buf(),
config.model_provider_id.clone(),
/*metrics*/ None,
)
.await
.map_err(std::io::Error::other)?;
@@ -2272,7 +2284,7 @@ mod tests {
CloudRequirementsLoader::default(),
codex_feedback::CodexFeedback::new(),
/*log_db*/ None,
/*state_db*/ None,
StateDbAccess::none(),
Arc::new(EnvironmentManager::default_for_tests()),
|_args| async { Err(std::io::Error::other("boom")) },
)
+1 -1
View File
@@ -1065,7 +1065,7 @@ mod tests {
.await,
feedback: codex_feedback::CodexFeedback::new(),
log_db: None,
state_db: None,
state_db_access: codex_app_server_client::StateDbAccess::none(),
environment_manager: Arc::new(
codex_app_server_client::EnvironmentManager::default_for_tests(),
),