mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
Merge branch 'main' into codex/stateful-apply-patch-streaming
This commit is contained in:
@@ -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::StateDbHandle;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::ClientNotification;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
@@ -343,6 +344,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>,
|
||||
/// Environment manager used by core execution and filesystem operations.
|
||||
pub environment_manager: Arc<EnvironmentManager>,
|
||||
/// Startup warnings emitted after initialize succeeds.
|
||||
@@ -404,6 +407,7 @@ impl InProcessClientStartArgs {
|
||||
thread_config_loader,
|
||||
feedback: self.feedback,
|
||||
log_db: self.log_db,
|
||||
state_db: self.state_db,
|
||||
environment_manager: self.environment_manager,
|
||||
config_warnings: self.config_warnings,
|
||||
session_source: self.session_source,
|
||||
@@ -983,6 +987,7 @@ mod tests {
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
state_db: None,
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
config_warnings: Vec::new(),
|
||||
session_source,
|
||||
@@ -2057,6 +2062,7 @@ mod tests {
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
state_db: None,
|
||||
environment_manager: environment_manager.clone(),
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::Exec,
|
||||
@@ -2096,6 +2102,7 @@ mod tests {
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
state_db: None,
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::Exec,
|
||||
|
||||
@@ -86,6 +86,7 @@ use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_login::AuthManager;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
pub use codex_rollout::StateDbHandle;
|
||||
pub use codex_state::log_db::LogDbLayer;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
@@ -126,6 +127,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>,
|
||||
/// Environment manager used by core execution and filesystem operations.
|
||||
pub environment_manager: Arc<EnvironmentManager>,
|
||||
/// Startup warnings emitted after initialize succeeds.
|
||||
@@ -251,6 +254,8 @@ pub struct InProcessClientHandle {
|
||||
client: InProcessClientSender,
|
||||
event_rx: mpsc::Receiver<InProcessServerEvent>,
|
||||
runtime_handle: tokio::task::JoinHandle<()>,
|
||||
#[cfg(test)]
|
||||
_test_codex_home: Option<tempfile::TempDir>,
|
||||
}
|
||||
|
||||
impl InProcessClientHandle {
|
||||
@@ -418,6 +423,7 @@ fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle {
|
||||
environment_manager: args.environment_manager,
|
||||
feedback: args.feedback,
|
||||
log_db: args.log_db,
|
||||
state_db: args.state_db,
|
||||
config_warnings: args.config_warnings,
|
||||
session_source: args.session_source,
|
||||
auth_manager,
|
||||
@@ -717,6 +723,8 @@ fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle {
|
||||
client: InProcessClientSender { client_tx },
|
||||
event_rx,
|
||||
runtime_handle,
|
||||
#[cfg(test)]
|
||||
_test_codex_home: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -738,13 +746,22 @@ mod tests {
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
|
||||
async fn build_test_config() -> Config {
|
||||
match ConfigBuilder::default().build().await {
|
||||
async fn build_test_config(codex_home: &Path) -> Config {
|
||||
match ConfigBuilder::default()
|
||||
.codex_home(codex_home.to_path_buf())
|
||||
.build()
|
||||
.await
|
||||
{
|
||||
Ok(config) => config,
|
||||
Err(_) => Config::load_default_with_cli_overrides(Vec::new())
|
||||
.await
|
||||
.expect("default config should load"),
|
||||
Err(_) => Config::load_default_with_cli_overrides_for_codex_home(
|
||||
codex_home.to_path_buf(),
|
||||
Vec::new(),
|
||||
)
|
||||
.await
|
||||
.expect("default config should load"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -752,15 +769,21 @@ mod tests {
|
||||
session_source: SessionSource,
|
||||
channel_capacity: usize,
|
||||
) -> 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())
|
||||
.await
|
||||
.expect("state db should initialize for in-process test");
|
||||
let args = InProcessStartArgs {
|
||||
arg0_paths: Arg0DispatchPaths::default(),
|
||||
config: Arc::new(build_test_config().await),
|
||||
config,
|
||||
cli_overrides: Vec::new(),
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
state_db: Some(state_db),
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
config_warnings: Vec::new(),
|
||||
session_source,
|
||||
@@ -775,7 +798,9 @@ mod tests {
|
||||
},
|
||||
channel_capacity,
|
||||
};
|
||||
start(args).await.expect("in-process runtime should start")
|
||||
let mut client = start(args).await.expect("in-process runtime should start");
|
||||
client._test_codex_home = Some(codex_home);
|
||||
client
|
||||
}
|
||||
|
||||
async fn start_test_client(session_source: SessionSource) -> InProcessClientHandle {
|
||||
|
||||
@@ -54,6 +54,7 @@ use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::ExecServerRuntimePaths;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_rollout::state_db as rollout_state_db;
|
||||
use codex_state::log_db;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
@@ -453,23 +454,6 @@ pub async fn run_main_with_transport_options(
|
||||
.await
|
||||
{
|
||||
Ok(config) => {
|
||||
let effective_toml = config.config_layer_stack.effective_config();
|
||||
match effective_toml.try_into() {
|
||||
Ok(config_toml) => {
|
||||
if let Err(err) = codex_core::personality_migration::maybe_migrate_personality(
|
||||
&config.codex_home,
|
||||
&config_toml,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(error = %err, "Failed to run personality migration");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(error = %err, "Failed to deserialize config for personality migration");
|
||||
}
|
||||
}
|
||||
|
||||
let discovered_thread_config_loader = configured_thread_config_loader(&config);
|
||||
config_manager
|
||||
.replace_thread_config_loader(Arc::clone(&discovered_thread_config_loader));
|
||||
@@ -483,23 +467,70 @@ pub async fn run_main_with_transport_options(
|
||||
}
|
||||
};
|
||||
let mut config_warnings = Vec::new();
|
||||
let config = match config_manager
|
||||
let (mut config, should_run_personality_migration) = match config_manager
|
||||
.load_latest_config(/*fallback_cwd*/ None)
|
||||
.await
|
||||
{
|
||||
Ok(config) => config,
|
||||
Ok(config) => (config, true),
|
||||
Err(err) => {
|
||||
let message = config_warning_from_error("Invalid configuration; using defaults.", &err);
|
||||
config_warnings.push(message);
|
||||
config_manager.load_default_config().await.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("error loading default config after config error: {e}"),
|
||||
)
|
||||
})?
|
||||
(
|
||||
config_manager.load_default_config().await.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("error loading default config after config error: {e}"),
|
||||
)
|
||||
})?,
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let state_db_result = rollout_state_db::try_init(&config).await;
|
||||
let state_db_init_error = state_db_result.as_ref().err().map(ToString::to_string);
|
||||
let state_db = state_db_result.ok();
|
||||
|
||||
if should_run_personality_migration {
|
||||
let effective_toml = config.config_layer_stack.effective_config();
|
||||
match effective_toml.try_into() {
|
||||
Ok(config_toml) => {
|
||||
match codex_core::personality_migration::maybe_migrate_personality(
|
||||
&config.codex_home,
|
||||
&config_toml,
|
||||
state_db.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(codex_core::personality_migration::PersonalityMigrationStatus::Applied) => {
|
||||
config = config_manager
|
||||
.load_latest_config(/*fallback_cwd*/ None)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
std::io::Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!(
|
||||
"error reloading config after personality migration: {err}"
|
||||
),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Ok(
|
||||
codex_core::personality_migration::PersonalityMigrationStatus::SkippedMarker
|
||||
| codex_core::personality_migration::PersonalityMigrationStatus::SkippedExplicitPersonality
|
||||
| codex_core::personality_migration::PersonalityMigrationStatus::SkippedNoSessions,
|
||||
) => {}
|
||||
Err(err) => {
|
||||
warn!(error = %err, "Failed to run personality migration");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(error = %err, "Failed to deserialize config for personality migration");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(Some(err)) = check_execpolicy_for_warnings(&config.config_layer_stack).await {
|
||||
let (path, range) = exec_policy_warning_location(&err);
|
||||
let message = ConfigWarningNotification {
|
||||
@@ -567,13 +598,6 @@ pub async fn run_main_with_transport_options(
|
||||
|
||||
let feedback_layer = feedback.logger_layer();
|
||||
let feedback_metadata_layer = feedback.metadata_layer();
|
||||
let state_db_result = codex_state::StateRuntime::init(
|
||||
config.sqlite_home.clone(),
|
||||
config.model_provider_id.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 log_db = state_db.clone().map(log_db::start);
|
||||
let log_db_layer = log_db
|
||||
.clone()
|
||||
@@ -745,6 +769,7 @@ pub async fn run_main_with_transport_options(
|
||||
environment_manager,
|
||||
feedback: feedback.clone(),
|
||||
log_db,
|
||||
state_db: state_db.clone(),
|
||||
config_warnings,
|
||||
session_source,
|
||||
auth_manager,
|
||||
|
||||
@@ -72,6 +72,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_state::log_db::LogDbLayer;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::Semaphore;
|
||||
@@ -251,6 +252,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) config_warnings: Vec<ConfigWarningNotification>,
|
||||
pub(crate) session_source: SessionSource,
|
||||
pub(crate) auth_manager: Arc<AuthManager>,
|
||||
@@ -272,6 +274,7 @@ impl MessageProcessor {
|
||||
environment_manager,
|
||||
feedback,
|
||||
log_db,
|
||||
state_db,
|
||||
config_warnings,
|
||||
session_source,
|
||||
auth_manager,
|
||||
@@ -285,7 +288,7 @@ 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());
|
||||
let thread_store = thread_store_from_config(config.as_ref(), state_db.clone());
|
||||
let thread_manager = Arc::new(ThreadManager::new(
|
||||
config.as_ref(),
|
||||
auth_manager.clone(),
|
||||
@@ -293,6 +296,7 @@ impl MessageProcessor {
|
||||
environment_manager,
|
||||
Some(analytics_events_client.clone()),
|
||||
Arc::clone(&thread_store),
|
||||
state_db.clone(),
|
||||
));
|
||||
thread_manager
|
||||
.plugins_manager()
|
||||
@@ -337,6 +341,7 @@ impl MessageProcessor {
|
||||
Arc::clone(&config),
|
||||
feedback,
|
||||
log_db,
|
||||
state_db.clone(),
|
||||
);
|
||||
let git_processor = GitRequestProcessor::new();
|
||||
let initialize_processor = InitializeRequestProcessor::new(
|
||||
@@ -371,6 +376,7 @@ impl MessageProcessor {
|
||||
outgoing.clone(),
|
||||
Arc::clone(&config),
|
||||
thread_state_manager.clone(),
|
||||
state_db.clone(),
|
||||
);
|
||||
let thread_processor = ThreadRequestProcessor::new(
|
||||
auth_manager.clone(),
|
||||
@@ -386,6 +392,7 @@ impl MessageProcessor {
|
||||
thread_watch_manager.clone(),
|
||||
Arc::clone(&thread_list_state_permit),
|
||||
thread_goal_processor.clone(),
|
||||
state_db.clone(),
|
||||
);
|
||||
let turn_processor = TurnRequestProcessor::new(
|
||||
auth_manager.clone(),
|
||||
@@ -399,6 +406,7 @@ impl MessageProcessor {
|
||||
thread_state_manager,
|
||||
thread_watch_manager,
|
||||
thread_list_state_permit,
|
||||
state_db.clone(),
|
||||
);
|
||||
if matches!(plugin_startup_tasks, crate::PluginStartupTasks::Start) {
|
||||
// Keep plugin startup warmups aligned at app-server startup.
|
||||
@@ -429,11 +437,7 @@ impl MessageProcessor {
|
||||
arg0_paths,
|
||||
config.codex_home.to_path_buf(),
|
||||
);
|
||||
let device_key_processor = DeviceKeyRequestProcessor::new(
|
||||
outgoing.clone(),
|
||||
config.sqlite_home.clone(),
|
||||
config.model_provider_id.clone(),
|
||||
);
|
||||
let device_key_processor = DeviceKeyRequestProcessor::new(outgoing.clone(), state_db);
|
||||
let fs_processor = FsRequestProcessor::new(
|
||||
thread_manager
|
||||
.environment_manager()
|
||||
|
||||
@@ -290,6 +290,7 @@ async fn build_test_processor(
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
state_db: None,
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::VSCode,
|
||||
auth_manager,
|
||||
|
||||
@@ -369,7 +369,6 @@ use codex_rmcp_client::perform_oauth_login_return_url;
|
||||
use codex_rollout::EventPersistenceMode;
|
||||
use codex_rollout::is_persisted_rollout_item;
|
||||
use codex_rollout::state_db::StateDbHandle;
|
||||
use codex_rollout::state_db::get_state_db;
|
||||
use codex_rollout::state_db::reconcile_rollout;
|
||||
use codex_state::StateRuntime;
|
||||
use codex_state::ThreadMetadata;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::error_code::internal_error;
|
||||
@@ -36,7 +35,6 @@ use codex_device_key::RemoteControlClientEnrollmentAudience;
|
||||
use codex_device_key::RemoteControlClientEnrollmentSignPayload;
|
||||
use codex_state::DeviceKeyBindingRecord;
|
||||
use codex_state::StateRuntime;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DeviceKeyRequestProcessor {
|
||||
@@ -47,15 +45,11 @@ pub(crate) struct DeviceKeyRequestProcessor {
|
||||
impl DeviceKeyRequestProcessor {
|
||||
pub(crate) fn new(
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
sqlite_home: PathBuf,
|
||||
default_provider: String,
|
||||
state_db: Option<Arc<StateRuntime>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
outgoing,
|
||||
store: DeviceKeyStore::new(Arc::new(StateDeviceKeyBindingStore::new(
|
||||
sqlite_home,
|
||||
default_provider,
|
||||
))),
|
||||
store: DeviceKeyStore::new(Arc::new(StateDeviceKeyBindingStore::new(state_db))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,39 +170,25 @@ async fn sign_device_key(
|
||||
}
|
||||
|
||||
struct StateDeviceKeyBindingStore {
|
||||
sqlite_home: PathBuf,
|
||||
default_provider: String,
|
||||
state_db: OnceCell<Arc<StateRuntime>>,
|
||||
state_db: Option<Arc<StateRuntime>>,
|
||||
}
|
||||
|
||||
impl StateDeviceKeyBindingStore {
|
||||
fn new(sqlite_home: PathBuf, default_provider: String) -> Self {
|
||||
Self {
|
||||
sqlite_home,
|
||||
default_provider,
|
||||
state_db: OnceCell::new(),
|
||||
}
|
||||
fn new(state_db: Option<Arc<StateRuntime>>) -> Self {
|
||||
Self { state_db }
|
||||
}
|
||||
|
||||
async fn state_db(&self) -> Result<Arc<StateRuntime>, DeviceKeyError> {
|
||||
let sqlite_home = self.sqlite_home.clone();
|
||||
let default_provider = self.default_provider.clone();
|
||||
self.state_db
|
||||
.get_or_try_init(|| async move {
|
||||
StateRuntime::init(sqlite_home, default_provider)
|
||||
.await
|
||||
.map_err(|err| DeviceKeyError::Platform(err.to_string()))
|
||||
})
|
||||
.await
|
||||
.cloned()
|
||||
.clone()
|
||||
.ok_or_else(|| DeviceKeyError::Platform("sqlite state db unavailable".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for StateDeviceKeyBindingStore {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("StateDeviceKeyBindingStore")
|
||||
.field("sqlite_home", &self.sqlite_home)
|
||||
.field("default_provider", &self.default_provider)
|
||||
.field("has_state_db", &self.state_db.is_some())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ pub(crate) struct FeedbackRequestProcessor {
|
||||
config: Arc<Config>,
|
||||
feedback: CodexFeedback,
|
||||
log_db: Option<LogDbLayer>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
}
|
||||
|
||||
impl FeedbackRequestProcessor {
|
||||
@@ -16,6 +17,7 @@ impl FeedbackRequestProcessor {
|
||||
config: Arc<Config>,
|
||||
feedback: CodexFeedback,
|
||||
log_db: Option<LogDbLayer>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) -> Self {
|
||||
Self {
|
||||
auth_manager,
|
||||
@@ -23,6 +25,7 @@ impl FeedbackRequestProcessor {
|
||||
config,
|
||||
feedback,
|
||||
log_db,
|
||||
state_db,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +78,7 @@ impl FeedbackRequestProcessor {
|
||||
if let Some(log_db) = self.log_db.as_ref() {
|
||||
log_db.flush().await;
|
||||
}
|
||||
let state_db_ctx = get_state_db(&self.config).await;
|
||||
let state_db_ctx = self.state_db.clone();
|
||||
let feedback_thread_ids = match conversation_id {
|
||||
Some(conversation_id) => match self
|
||||
.thread_manager
|
||||
|
||||
@@ -7,6 +7,7 @@ pub(crate) struct ThreadGoalRequestProcessor {
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
config: Arc<Config>,
|
||||
thread_state_manager: ThreadStateManager,
|
||||
state_db: Option<StateDbHandle>,
|
||||
}
|
||||
|
||||
impl ThreadGoalRequestProcessor {
|
||||
@@ -15,12 +16,14 @@ impl ThreadGoalRequestProcessor {
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
config: Arc<Config>,
|
||||
thread_state_manager: ThreadStateManager,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) -> Self {
|
||||
Self {
|
||||
thread_manager,
|
||||
outgoing,
|
||||
config,
|
||||
thread_state_manager,
|
||||
state_db,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +81,7 @@ impl ThreadGoalRequestProcessor {
|
||||
if let Some(state_db) = thread.state_db() {
|
||||
Some(state_db)
|
||||
} else {
|
||||
open_state_db_for_direct_thread_lookup(&self.config).await
|
||||
self.state_db.clone()
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -104,12 +107,16 @@ impl ThreadGoalRequestProcessor {
|
||||
"ephemeral thread does not support goals: {thread_id}"
|
||||
))
|
||||
})?,
|
||||
None => find_thread_path_by_id_str(&self.config.codex_home, &thread_id.to_string())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
internal_error(format!("failed to locate thread id {thread_id}: {err}"))
|
||||
})?
|
||||
.ok_or_else(|| invalid_request(format!("thread not found: {thread_id}")))?,
|
||||
None => find_thread_path_by_id_str(
|
||||
&self.config.codex_home,
|
||||
&thread_id.to_string(),
|
||||
self.state_db.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
internal_error(format!("failed to locate thread id {thread_id}: {err}"))
|
||||
})?
|
||||
.ok_or_else(|| invalid_request(format!("thread not found: {thread_id}")))?,
|
||||
};
|
||||
reconcile_rollout(
|
||||
Some(&state_db),
|
||||
@@ -247,12 +254,16 @@ impl ThreadGoalRequestProcessor {
|
||||
"ephemeral thread does not support goals: {thread_id}"
|
||||
))
|
||||
})?,
|
||||
None => find_thread_path_by_id_str(&self.config.codex_home, &thread_id.to_string())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
internal_error(format!("failed to locate thread id {thread_id}: {err}"))
|
||||
})?
|
||||
.ok_or_else(|| invalid_request(format!("thread not found: {thread_id}")))?,
|
||||
None => find_thread_path_by_id_str(
|
||||
&self.config.codex_home,
|
||||
&thread_id.to_string(),
|
||||
self.state_db.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
internal_error(format!("failed to locate thread id {thread_id}: {err}"))
|
||||
})?
|
||||
.ok_or_else(|| invalid_request(format!("thread not found: {thread_id}")))?,
|
||||
};
|
||||
reconcile_rollout(
|
||||
Some(&state_db),
|
||||
@@ -307,16 +318,20 @@ impl ThreadGoalRequestProcessor {
|
||||
return Ok(state_db);
|
||||
}
|
||||
} else {
|
||||
find_thread_path_by_id_str(&self.config.codex_home, &thread_id.to_string())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
internal_error(format!("failed to locate thread id {thread_id}: {err}"))
|
||||
})?
|
||||
.ok_or_else(|| invalid_request(format!("thread not found: {thread_id}")))?;
|
||||
find_thread_path_by_id_str(
|
||||
&self.config.codex_home,
|
||||
&thread_id.to_string(),
|
||||
self.state_db.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
internal_error(format!("failed to locate thread id {thread_id}: {err}"))
|
||||
})?
|
||||
.ok_or_else(|| invalid_request(format!("thread not found: {thread_id}")))?;
|
||||
}
|
||||
|
||||
open_state_db_for_direct_thread_lookup(&self.config)
|
||||
.await
|
||||
self.state_db
|
||||
.clone()
|
||||
.ok_or_else(|| internal_error("sqlite state db unavailable for thread goals"))
|
||||
}
|
||||
|
||||
|
||||
@@ -259,6 +259,7 @@ pub(crate) struct ThreadRequestProcessor {
|
||||
pub(super) thread_watch_manager: ThreadWatchManager,
|
||||
pub(super) thread_list_state_permit: Arc<Semaphore>,
|
||||
pub(super) thread_goal_processor: ThreadGoalRequestProcessor,
|
||||
pub(super) state_db: Option<StateDbHandle>,
|
||||
pub(super) background_tasks: TaskTracker,
|
||||
}
|
||||
|
||||
@@ -278,6 +279,7 @@ impl ThreadRequestProcessor {
|
||||
thread_watch_manager: ThreadWatchManager,
|
||||
thread_list_state_permit: Arc<Semaphore>,
|
||||
thread_goal_processor: ThreadGoalRequestProcessor,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) -> Self {
|
||||
Self {
|
||||
auth_manager,
|
||||
@@ -293,6 +295,7 @@ impl ThreadRequestProcessor {
|
||||
thread_watch_manager,
|
||||
thread_list_state_permit,
|
||||
thread_goal_processor,
|
||||
state_db,
|
||||
background_tasks: TaskTracker::new(),
|
||||
}
|
||||
}
|
||||
@@ -1170,7 +1173,7 @@ impl ThreadRequestProcessor {
|
||||
.map_err(|err| invalid_request(format!("invalid thread id: {err}")))?;
|
||||
|
||||
let mut thread_ids = vec![thread_id];
|
||||
if let Some(state_db_ctx) = get_state_db(&self.config).await {
|
||||
if let Some(state_db_ctx) = self.state_db.as_ref() {
|
||||
let descendants = state_db_ctx
|
||||
.list_thread_spawn_descendants(thread_id)
|
||||
.await
|
||||
@@ -1391,14 +1394,10 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
|
||||
async fn memory_reset_response_inner(&self) -> Result<MemoryResetResponse, JSONRPCErrorError> {
|
||||
let state_db = StateRuntime::init(
|
||||
self.config.sqlite_home.clone(),
|
||||
self.config.model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
internal_error(format!("failed to open state db for memory reset: {err}"))
|
||||
})?;
|
||||
let state_db = self
|
||||
.state_db
|
||||
.clone()
|
||||
.ok_or_else(|| internal_error("sqlite state db unavailable for memory reset"))?;
|
||||
|
||||
state_db.clear_memory_data().await.map_err(|err| {
|
||||
internal_error(format!("failed to clear memory rows in state db: {err}"))
|
||||
@@ -1445,7 +1444,7 @@ impl ThreadRequestProcessor {
|
||||
let loaded_thread = self.thread_manager.get_thread(thread_uuid).await.ok();
|
||||
let mut state_db_ctx = loaded_thread.as_ref().and_then(|thread| thread.state_db());
|
||||
if state_db_ctx.is_none() {
|
||||
state_db_ctx = get_state_db(&self.config).await;
|
||||
state_db_ctx = self.state_db.clone();
|
||||
}
|
||||
let Some(state_db_ctx) = state_db_ctx else {
|
||||
return Err(internal_error(format!(
|
||||
@@ -1583,33 +1582,37 @@ impl ThreadRequestProcessor {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rollout_path =
|
||||
match find_thread_path_by_id_str(&self.config.codex_home, &thread_uuid.to_string())
|
||||
.await
|
||||
let rollout_path = match find_thread_path_by_id_str(
|
||||
&self.config.codex_home,
|
||||
&thread_uuid.to_string(),
|
||||
self.state_db.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(path)) => path,
|
||||
Ok(None) => match find_archived_thread_path_by_id_str(
|
||||
&self.config.codex_home,
|
||||
&thread_uuid.to_string(),
|
||||
self.state_db.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(path)) => path,
|
||||
Ok(None) => match find_archived_thread_path_by_id_str(
|
||||
&self.config.codex_home,
|
||||
&thread_uuid.to_string(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(path)) => path,
|
||||
Ok(None) => {
|
||||
return Err(invalid_request(format!("thread not found: {thread_uuid}")));
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(internal_error(format!(
|
||||
"failed to locate archived thread id {thread_uuid}: {err}"
|
||||
)));
|
||||
}
|
||||
},
|
||||
Ok(None) => {
|
||||
return Err(invalid_request(format!("thread not found: {thread_uuid}")));
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(internal_error(format!(
|
||||
"failed to locate thread id {thread_uuid}: {err}"
|
||||
"failed to locate archived thread id {thread_uuid}: {err}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
},
|
||||
Err(err) => {
|
||||
return Err(internal_error(format!(
|
||||
"failed to locate thread id {thread_uuid}: {err}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
reconcile_rollout(
|
||||
Some(state_db_ctx),
|
||||
@@ -2555,7 +2558,7 @@ impl ThreadRequestProcessor {
|
||||
let InitialHistory::Resumed(resumed_history) = thread_history else {
|
||||
return None;
|
||||
};
|
||||
let state_db_ctx = get_state_db(&self.config).await?;
|
||||
let state_db_ctx = self.state_db.clone()?;
|
||||
let persisted_metadata = state_db_ctx
|
||||
.get_thread(resumed_history.conversation_id)
|
||||
.await
|
||||
@@ -2922,7 +2925,9 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
|
||||
async fn attach_thread_name(&self, thread_id: ThreadId, thread: &mut Thread) {
|
||||
if let Some(title) = title_from_state_db(&self.config, thread_id).await {
|
||||
if let Some(title) =
|
||||
title_from_state_db(&self.config, self.state_db.as_ref(), thread_id).await
|
||||
{
|
||||
set_thread_name_from_title(thread, title);
|
||||
}
|
||||
}
|
||||
@@ -3683,8 +3688,12 @@ async fn read_summary_from_state_db_context_by_thread_id(
|
||||
Some(summary_from_thread_metadata(&metadata))
|
||||
}
|
||||
|
||||
async fn title_from_state_db(config: &Config, thread_id: ThreadId) -> Option<String> {
|
||||
if let Some(state_db_ctx) = open_state_db_for_direct_thread_lookup(config).await
|
||||
async fn title_from_state_db(
|
||||
config: &Config,
|
||||
state_db_ctx: Option<&StateDbHandle>,
|
||||
thread_id: ThreadId,
|
||||
) -> Option<String> {
|
||||
if let Some(state_db_ctx) = state_db_ctx
|
||||
&& let Some(metadata) = state_db_ctx.get_thread(thread_id).await.ok().flatten()
|
||||
&& let Some(title) = distinct_title(&metadata)
|
||||
{
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
use super::*;
|
||||
|
||||
pub(super) async fn open_state_db_for_direct_thread_lookup(
|
||||
config: &Config,
|
||||
) -> Option<StateDbHandle> {
|
||||
StateRuntime::init(config.sqlite_home.clone(), config.model_provider_id.clone())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub(crate) async fn read_summary_from_rollout(
|
||||
path: &Path,
|
||||
fallback_provider: &str,
|
||||
|
||||
@@ -13,6 +13,7 @@ pub(crate) struct TurnRequestProcessor {
|
||||
thread_state_manager: ThreadStateManager,
|
||||
thread_watch_manager: ThreadWatchManager,
|
||||
thread_list_state_permit: Arc<Semaphore>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
}
|
||||
|
||||
impl TurnRequestProcessor {
|
||||
@@ -29,6 +30,7 @@ impl TurnRequestProcessor {
|
||||
thread_state_manager: ThreadStateManager,
|
||||
thread_watch_manager: ThreadWatchManager,
|
||||
thread_list_state_permit: Arc<Semaphore>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) -> Self {
|
||||
Self {
|
||||
auth_manager,
|
||||
@@ -42,6 +44,7 @@ impl TurnRequestProcessor {
|
||||
thread_state_manager,
|
||||
thread_watch_manager,
|
||||
thread_list_state_permit,
|
||||
state_db,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -891,16 +894,20 @@ impl TurnRequestProcessor {
|
||||
let rollout_path = if let Some(path) = parent_thread.rollout_path() {
|
||||
path
|
||||
} else {
|
||||
find_thread_path_by_id_str(&self.config.codex_home, &parent_thread_id.to_string())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
internal_error(format!(
|
||||
"failed to locate thread id {parent_thread_id}: {err}"
|
||||
))
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
invalid_request(format!("no rollout found for thread id {parent_thread_id}"))
|
||||
})?
|
||||
find_thread_path_by_id_str(
|
||||
&self.config.codex_home,
|
||||
&parent_thread_id.to_string(),
|
||||
self.state_db.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
internal_error(format!(
|
||||
"failed to locate thread id {parent_thread_id}: {err}"
|
||||
))
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
invalid_request(format!("no rollout found for thread id {parent_thread_id}"))
|
||||
})?
|
||||
};
|
||||
|
||||
let mut config = self.config.as_ref().clone();
|
||||
|
||||
@@ -204,6 +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,
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::Cli,
|
||||
|
||||
@@ -80,6 +80,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,
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::Cli,
|
||||
|
||||
@@ -63,7 +63,7 @@ async fn thread_archive_requires_materialized_rollout() -> Result<()> {
|
||||
rollout_path.display()
|
||||
);
|
||||
assert!(
|
||||
find_thread_path_by_id_str(codex_home.path(), &thread.id)
|
||||
find_thread_path_by_id_str(codex_home.path(), &thread.id, /*state_db_ctx*/ None)
|
||||
.await?
|
||||
.is_none(),
|
||||
"thread id should not be discoverable before rollout materialization"
|
||||
@@ -118,9 +118,10 @@ 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)
|
||||
.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, /*state_db_ctx*/ 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
|
||||
@@ -252,15 +253,23 @@ async fn thread_archive_archives_spawned_descendants() -> Result<()> {
|
||||
|
||||
for thread_id in [parent_thread_id, child_thread_id, grandchild_thread_id] {
|
||||
assert!(
|
||||
find_thread_path_by_id_str(codex_home.path(), &thread_id.to_string())
|
||||
.await?
|
||||
.is_none(),
|
||||
find_thread_path_by_id_str(
|
||||
codex_home.path(),
|
||||
&thread_id.to_string(),
|
||||
/*state_db_ctx*/ None,
|
||||
)
|
||||
.await?
|
||||
.is_none(),
|
||||
"expected active rollout for {thread_id} to be archived"
|
||||
);
|
||||
assert!(
|
||||
find_archived_thread_path_by_id_str(codex_home.path(), &thread_id.to_string())
|
||||
.await?
|
||||
.is_some(),
|
||||
find_archived_thread_path_by_id_str(
|
||||
codex_home.path(),
|
||||
&thread_id.to_string(),
|
||||
/*state_db_ctx*/ None,
|
||||
)
|
||||
.await?
|
||||
.is_some(),
|
||||
"expected archived rollout for {thread_id} to exist"
|
||||
);
|
||||
}
|
||||
@@ -322,9 +331,10 @@ 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)
|
||||
.await?
|
||||
.expect("child rollout path");
|
||||
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 archived_child_path = codex_home
|
||||
.path()
|
||||
.join(ARCHIVED_SESSIONS_SUBDIR)
|
||||
@@ -381,15 +391,23 @@ async fn thread_archive_succeeds_when_descendant_archive_fails() -> Result<()> {
|
||||
);
|
||||
for thread_id in [parent_thread_id, grandchild_thread_id] {
|
||||
assert!(
|
||||
find_thread_path_by_id_str(codex_home.path(), &thread_id.to_string())
|
||||
.await?
|
||||
.is_none(),
|
||||
find_thread_path_by_id_str(
|
||||
codex_home.path(),
|
||||
&thread_id.to_string(),
|
||||
/*state_db_ctx*/ None,
|
||||
)
|
||||
.await?
|
||||
.is_none(),
|
||||
"expected active rollout for {thread_id} to be archived"
|
||||
);
|
||||
assert!(
|
||||
find_archived_thread_path_by_id_str(codex_home.path(), &thread_id.to_string())
|
||||
.await?
|
||||
.is_some(),
|
||||
find_archived_thread_path_by_id_str(
|
||||
codex_home.path(),
|
||||
&thread_id.to_string(),
|
||||
/*state_db_ctx*/ None,
|
||||
)
|
||||
.await?
|
||||
.is_some(),
|
||||
"expected archived rollout for {thread_id} to exist"
|
||||
);
|
||||
}
|
||||
@@ -455,15 +473,19 @@ 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)
|
||||
find_thread_path_by_id_str(codex_home.path(), &parent_id, /*state_db_ctx*/ 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)
|
||||
.await?
|
||||
.is_some(),
|
||||
find_archived_thread_path_by_id_str(
|
||||
codex_home.path(),
|
||||
&parent_id,
|
||||
/*state_db_ctx*/ None,
|
||||
)
|
||||
.await?
|
||||
.is_some(),
|
||||
"parent should be moved into archived sessions"
|
||||
);
|
||||
|
||||
|
||||
@@ -614,6 +614,7 @@ sqlite = true
|
||||
generate_memories: false,
|
||||
};
|
||||
let repaired_page = codex_core::RolloutRecorder::list_threads(
|
||||
Some(state_db.clone()),
|
||||
&rollout_config,
|
||||
/*page_size*/ 10,
|
||||
/*cursor*/ None,
|
||||
|
||||
@@ -211,9 +211,10 @@ async fn thread_name_update_rollout_count(
|
||||
codex_home: &Path,
|
||||
conversation_id: &str,
|
||||
) -> Result<usize> {
|
||||
let rollout_path = find_thread_path_by_id_str(codex_home, conversation_id)
|
||||
.await?
|
||||
.context("rollout path")?;
|
||||
let rollout_path =
|
||||
find_thread_path_by_id_str(codex_home, conversation_id, /*state_db_ctx*/ None)
|
||||
.await?
|
||||
.context("rollout path")?;
|
||||
let contents = tokio::fs::read_to_string(rollout_path).await?;
|
||||
Ok(contents
|
||||
.lines()
|
||||
|
||||
@@ -300,6 +300,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,
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::Cli.into(),
|
||||
@@ -363,6 +364,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,
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::Cli.into(),
|
||||
@@ -447,6 +449,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,
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::Cli.into(),
|
||||
|
||||
@@ -75,9 +75,10 @@ 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)
|
||||
.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, /*state_db_ctx*/ 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
|
||||
@@ -92,9 +93,13 @@ async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result
|
||||
.await??;
|
||||
let _: ThreadArchiveResponse = to_response::<ThreadArchiveResponse>(archive_resp)?;
|
||||
|
||||
let archived_path = find_archived_thread_path_by_id_str(codex_home.path(), &thread.id)
|
||||
.await?
|
||||
.expect("expected archived rollout path for thread id to exist");
|
||||
let archived_path = find_archived_thread_path_by_id_str(
|
||||
codex_home.path(),
|
||||
&thread.id,
|
||||
/*state_db_ctx*/ None,
|
||||
)
|
||||
.await?
|
||||
.expect("expected archived rollout path for thread id to exist");
|
||||
let archived_path_display = archived_path.display();
|
||||
assert!(
|
||||
archived_path.exists(),
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::MaybeApplyPatchVerified;
|
||||
use crate::parser::Hunk;
|
||||
use crate::parser::ParseError;
|
||||
use crate::parser::ParsePatchMode;
|
||||
use crate::parser::parse_patch_with_mode;
|
||||
use crate::parser::parse_patch;
|
||||
use crate::unified_diff_from_chunks;
|
||||
use std::str::Utf8Error;
|
||||
use tree_sitter::LanguageError;
|
||||
@@ -103,16 +103,11 @@ fn extract_apply_patch_from_shell(
|
||||
}
|
||||
|
||||
// TODO: make private once we remove tests in lib.rs
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
|
||||
maybe_parse_apply_patch_with_mode(argv, ParsePatchMode::Legacy)
|
||||
}
|
||||
|
||||
pub fn maybe_parse_apply_patch_with_mode(argv: &[String], mode: ParsePatchMode) -> MaybeApplyPatch {
|
||||
pub fn maybe_parse_apply_patch(argv: &[String], mode: ParsePatchMode) -> MaybeApplyPatch {
|
||||
match argv {
|
||||
// Direct invocation: apply_patch <patch>
|
||||
[cmd, body] if APPLY_PATCH_COMMANDS.contains(&cmd.as_str()) => {
|
||||
match parse_patch_with_mode(body, mode) {
|
||||
match parse_patch(body, mode) {
|
||||
Ok(source) => MaybeApplyPatch::Body(source),
|
||||
Err(e) => MaybeApplyPatch::PatchParseError(e),
|
||||
}
|
||||
@@ -120,7 +115,7 @@ pub fn maybe_parse_apply_patch_with_mode(argv: &[String], mode: ParsePatchMode)
|
||||
// Shell heredoc form: (optional `cd <path> &&`) apply_patch <<'EOF' ...
|
||||
_ => match parse_shell_script(argv) {
|
||||
Some((shell, script)) => match extract_apply_patch_from_shell(shell, script) {
|
||||
Ok((body, workdir)) => match parse_patch_with_mode(&body, mode) {
|
||||
Ok((body, workdir)) => match parse_patch(&body, mode) {
|
||||
Ok(mut source) => {
|
||||
source.workdir = workdir;
|
||||
MaybeApplyPatch::Body(source)
|
||||
@@ -140,15 +135,6 @@ pub fn maybe_parse_apply_patch_with_mode(argv: &[String], mode: ParsePatchMode)
|
||||
/// cwd must be an absolute path so that we can resolve relative paths in the
|
||||
/// patch.
|
||||
pub async fn maybe_parse_apply_patch_verified(
|
||||
argv: &[String],
|
||||
cwd: &AbsolutePathBuf,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&codex_exec_server::FileSystemSandboxContext>,
|
||||
) -> MaybeApplyPatchVerified {
|
||||
maybe_parse_apply_patch_verified_with_mode(argv, ParsePatchMode::Legacy, cwd, fs, sandbox).await
|
||||
}
|
||||
|
||||
pub async fn maybe_parse_apply_patch_verified_with_mode(
|
||||
argv: &[String],
|
||||
mode: ParsePatchMode,
|
||||
cwd: &AbsolutePathBuf,
|
||||
@@ -158,17 +144,17 @@ pub async fn maybe_parse_apply_patch_verified_with_mode(
|
||||
// Detect a raw patch body passed directly as the command or as the body of a shell
|
||||
// script. In these cases, report an explicit error rather than applying the patch.
|
||||
if let [body] = argv
|
||||
&& parse_patch_with_mode(body, mode).is_ok()
|
||||
&& parse_patch(body, mode).is_ok()
|
||||
{
|
||||
return MaybeApplyPatchVerified::CorrectnessError(ApplyPatchError::ImplicitInvocation);
|
||||
}
|
||||
if let Some((_, script)) = parse_shell_script(argv)
|
||||
&& parse_patch_with_mode(script, mode).is_ok()
|
||||
&& parse_patch(script, mode).is_ok()
|
||||
{
|
||||
return MaybeApplyPatchVerified::CorrectnessError(ApplyPatchError::ImplicitInvocation);
|
||||
}
|
||||
|
||||
match maybe_parse_apply_patch_with_mode(argv, mode) {
|
||||
match maybe_parse_apply_patch(argv, mode) {
|
||||
MaybeApplyPatch::Body(ApplyPatchArgs {
|
||||
patch,
|
||||
hunks,
|
||||
@@ -179,15 +165,13 @@ pub async fn maybe_parse_apply_patch_verified_with_mode(
|
||||
.map(|dir| cwd.join(Path::new(dir)))
|
||||
.unwrap_or_else(|| cwd.clone());
|
||||
let mut changes = HashMap::new();
|
||||
for hunk in &hunks {
|
||||
for hunk in hunks {
|
||||
let path = hunk.resolve_path(&effective_cwd);
|
||||
match hunk {
|
||||
Hunk::AddFile { contents, .. } => {
|
||||
changes.insert(
|
||||
path.into_path_buf(),
|
||||
ApplyPatchFileChange::Add {
|
||||
content: contents.clone(),
|
||||
},
|
||||
ApplyPatchFileChange::Add { content: contents },
|
||||
);
|
||||
}
|
||||
Hunk::DeleteFile { .. } => {
|
||||
@@ -223,9 +207,7 @@ pub async fn maybe_parse_apply_patch_verified_with_mode(
|
||||
path.into_path_buf(),
|
||||
ApplyPatchFileChange::Update {
|
||||
unified_diff,
|
||||
move_path: move_path
|
||||
.as_ref()
|
||||
.map(|p| effective_cwd.join(p).into_path_buf()),
|
||||
move_path: move_path.map(|p| effective_cwd.join(p).into_path_buf()),
|
||||
new_content: contents,
|
||||
},
|
||||
);
|
||||
@@ -234,7 +216,6 @@ pub async fn maybe_parse_apply_patch_verified_with_mode(
|
||||
}
|
||||
MaybeApplyPatchVerified::Body(ApplyPatchAction {
|
||||
changes,
|
||||
hunks,
|
||||
patch,
|
||||
cwd: effective_cwd,
|
||||
})
|
||||
@@ -400,7 +381,6 @@ fn extract_apply_patch_from_bash(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::UpdateFileChunk;
|
||||
use crate::parse_patch;
|
||||
use crate::unified_diff_from_chunks;
|
||||
use assert_matches::assert_matches;
|
||||
@@ -462,7 +442,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn assert_match_args(args: Vec<String>, expected_workdir: Option<&str>) {
|
||||
match maybe_parse_apply_patch(&args) {
|
||||
match maybe_parse_apply_patch(&args, ParsePatchMode::Legacy) {
|
||||
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, workdir, .. }) => {
|
||||
assert_eq!(workdir.as_deref(), expected_workdir);
|
||||
assert_eq!(hunks, expected_single_add());
|
||||
@@ -479,7 +459,7 @@ mod tests {
|
||||
fn assert_not_match(script: &str) {
|
||||
let args = args_bash(script);
|
||||
assert_matches!(
|
||||
maybe_parse_apply_patch(&args),
|
||||
maybe_parse_apply_patch(&args, ParsePatchMode::Legacy),
|
||||
MaybeApplyPatch::NotApplyPatch
|
||||
);
|
||||
}
|
||||
@@ -492,6 +472,7 @@ mod tests {
|
||||
assert_matches!(
|
||||
maybe_parse_apply_patch_verified(
|
||||
&args,
|
||||
ParsePatchMode::Legacy,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
LOCAL_FS.as_ref(),
|
||||
/*sandbox*/ None,
|
||||
@@ -509,6 +490,7 @@ mod tests {
|
||||
assert_matches!(
|
||||
maybe_parse_apply_patch_verified(
|
||||
&args,
|
||||
ParsePatchMode::Legacy,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
LOCAL_FS.as_ref(),
|
||||
/*sandbox*/ None,
|
||||
@@ -529,7 +511,7 @@ mod tests {
|
||||
"#,
|
||||
]);
|
||||
|
||||
match maybe_parse_apply_patch(&args) {
|
||||
match maybe_parse_apply_patch(&args, ParsePatchMode::Legacy) {
|
||||
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, .. }) => {
|
||||
assert_eq!(
|
||||
hunks,
|
||||
@@ -554,7 +536,7 @@ mod tests {
|
||||
"#,
|
||||
]);
|
||||
|
||||
match maybe_parse_apply_patch(&args) {
|
||||
match maybe_parse_apply_patch(&args, ParsePatchMode::Legacy) {
|
||||
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, .. }) => {
|
||||
assert_eq!(
|
||||
hunks,
|
||||
@@ -593,7 +575,7 @@ mod tests {
|
||||
PATCH"#,
|
||||
]);
|
||||
|
||||
match maybe_parse_apply_patch(&args) {
|
||||
match maybe_parse_apply_patch(&args, ParsePatchMode::Legacy) {
|
||||
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, workdir, .. }) => {
|
||||
assert_eq!(workdir, None);
|
||||
assert_eq!(
|
||||
@@ -714,7 +696,7 @@ PATCH"#,
|
||||
path.display()
|
||||
));
|
||||
|
||||
let patch = parse_patch(&patch).unwrap();
|
||||
let patch = parse_patch(&patch, ParsePatchMode::Legacy).unwrap();
|
||||
let chunks = match patch.hunks.as_slice() {
|
||||
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
||||
_ => panic!("Expected a single UpdateFile hunk"),
|
||||
@@ -753,7 +735,7 @@ PATCH"#,
|
||||
path.display()
|
||||
));
|
||||
|
||||
let patch = parse_patch(&patch).unwrap();
|
||||
let patch = parse_patch(&patch, ParsePatchMode::Legacy).unwrap();
|
||||
let chunks = match patch.hunks.as_slice() {
|
||||
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
||||
_ => panic!("Expected a single UpdateFile hunk"),
|
||||
@@ -798,6 +780,7 @@ PATCH"#,
|
||||
|
||||
let result = maybe_parse_apply_patch_verified(
|
||||
&argv,
|
||||
ParsePatchMode::Legacy,
|
||||
&AbsolutePathBuf::from_absolute_path(session_dir.path()).unwrap(),
|
||||
LOCAL_FS.as_ref(),
|
||||
/*sandbox*/ None,
|
||||
@@ -821,16 +804,6 @@ PATCH"#,
|
||||
new_content: "updated session directory content\n".to_string(),
|
||||
},
|
||||
)]),
|
||||
hunks: vec![Hunk::UpdateFile {
|
||||
path: PathBuf::from(relative_path),
|
||||
move_path: None,
|
||||
chunks: vec![UpdateFileChunk {
|
||||
change_context: None,
|
||||
old_lines: vec!["session directory content".to_string()],
|
||||
new_lines: vec!["updated session directory content".to_string()],
|
||||
is_end_of_file: false,
|
||||
}],
|
||||
}],
|
||||
patch: argv[1].clone(),
|
||||
cwd: AbsolutePathBuf::from_absolute_path(session_dir.path()).unwrap(),
|
||||
})
|
||||
@@ -862,6 +835,7 @@ PATCH"#,
|
||||
|
||||
let result = maybe_parse_apply_patch_verified(
|
||||
&argv,
|
||||
ParsePatchMode::Legacy,
|
||||
&AbsolutePathBuf::from_absolute_path(session_dir.path()).unwrap(),
|
||||
LOCAL_FS.as_ref(),
|
||||
/*sandbox*/ None,
|
||||
|
||||
@@ -27,7 +27,6 @@ pub use streaming_parser::StreamingPatchParser;
|
||||
use thiserror::Error;
|
||||
|
||||
pub use invocation::maybe_parse_apply_patch_verified;
|
||||
pub use invocation::maybe_parse_apply_patch_verified_with_mode;
|
||||
pub use standalone_executable::main;
|
||||
|
||||
use crate::invocation::ExtractHeredocError;
|
||||
@@ -137,7 +136,6 @@ pub enum MaybeApplyPatchVerified {
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ApplyPatchAction {
|
||||
changes: HashMap<PathBuf, ApplyPatchFileChange>,
|
||||
hunks: Vec<Hunk>,
|
||||
|
||||
/// The raw patch argument that can be used to apply the patch. i.e., if the
|
||||
/// original arg was parsed in "lenient" mode with a
|
||||
@@ -158,10 +156,6 @@ impl ApplyPatchAction {
|
||||
&self.changes
|
||||
}
|
||||
|
||||
pub fn hunks(&self) -> &[Hunk] {
|
||||
&self.hunks
|
||||
}
|
||||
|
||||
/// Should be used exclusively for testing. (Not worth the overhead of
|
||||
/// creating a feature flag for this.)
|
||||
pub fn new_add_for_test(path: &AbsolutePathBuf, content: String) -> Self {
|
||||
@@ -177,15 +171,10 @@ impl ApplyPatchAction {
|
||||
+ {content}
|
||||
*** End Patch"#,
|
||||
);
|
||||
let hunk = Hunk::AddFile {
|
||||
path: path.to_path_buf(),
|
||||
contents: content.clone(),
|
||||
};
|
||||
let changes = HashMap::from([(path.to_path_buf(), ApplyPatchFileChange::Add { content })]);
|
||||
#[expect(clippy::expect_used)]
|
||||
Self {
|
||||
changes,
|
||||
hunks: vec![hunk],
|
||||
cwd: path.parent().expect("path should have parent"),
|
||||
patch,
|
||||
}
|
||||
@@ -195,13 +184,14 @@ impl ApplyPatchAction {
|
||||
/// Applies the patch and prints the result to stdout/stderr.
|
||||
pub async fn apply_patch(
|
||||
patch: &str,
|
||||
mode: ParsePatchMode,
|
||||
cwd: &AbsolutePathBuf,
|
||||
stdout: &mut impl std::io::Write,
|
||||
stderr: &mut impl std::io::Write,
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> Result<(), ApplyPatchError> {
|
||||
let hunks = match parse_patch(patch) {
|
||||
let hunks = match parser::parse_patch(patch, mode) {
|
||||
Ok(source) => source.hunks,
|
||||
Err(e) => {
|
||||
match &e {
|
||||
@@ -652,6 +642,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
ParsePatchMode::Legacy,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
@@ -712,6 +703,7 @@ mod tests {
|
||||
|
||||
apply_patch(
|
||||
&patch,
|
||||
ParsePatchMode::Legacy,
|
||||
&cwd,
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
@@ -755,6 +747,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
ParsePatchMode::Legacy,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
@@ -791,6 +784,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
ParsePatchMode::Legacy,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
@@ -831,6 +825,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
ParsePatchMode::Legacy,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
@@ -880,6 +875,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
ParsePatchMode::Legacy,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
@@ -938,6 +934,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
ParsePatchMode::Legacy,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
@@ -982,6 +979,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
ParsePatchMode::Legacy,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
@@ -1025,6 +1023,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
ParsePatchMode::Legacy,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
@@ -1069,7 +1068,7 @@ mod tests {
|
||||
+QUX"#,
|
||||
path.display()
|
||||
));
|
||||
let patch = parse_patch(&patch).unwrap();
|
||||
let patch = parse_patch(&patch, ParsePatchMode::Legacy).unwrap();
|
||||
|
||||
let update_file_chunks = match patch.hunks.as_slice() {
|
||||
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
||||
@@ -1116,7 +1115,7 @@ mod tests {
|
||||
path.display()
|
||||
));
|
||||
|
||||
let patch = parse_patch(&patch).unwrap();
|
||||
let patch = parse_patch(&patch, ParsePatchMode::Legacy).unwrap();
|
||||
let chunks = match patch.hunks.as_slice() {
|
||||
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
||||
_ => panic!("Expected a single UpdateFile hunk"),
|
||||
@@ -1157,7 +1156,7 @@ mod tests {
|
||||
path.display()
|
||||
));
|
||||
|
||||
let patch = parse_patch(&patch).unwrap();
|
||||
let patch = parse_patch(&patch, ParsePatchMode::Legacy).unwrap();
|
||||
let chunks = match patch.hunks.as_slice() {
|
||||
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
||||
_ => panic!("Expected a single UpdateFile hunk"),
|
||||
@@ -1196,7 +1195,7 @@ mod tests {
|
||||
path.display()
|
||||
));
|
||||
|
||||
let patch = parse_patch(&patch).unwrap();
|
||||
let patch = parse_patch(&patch, ParsePatchMode::Legacy).unwrap();
|
||||
let chunks = match patch.hunks.as_slice() {
|
||||
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
||||
_ => panic!("Expected a single UpdateFile hunk"),
|
||||
@@ -1246,7 +1245,7 @@ mod tests {
|
||||
let patch = wrap_patch(&patch_body);
|
||||
|
||||
// Extract chunks then build the unified diff.
|
||||
let parsed = parse_patch(&patch).unwrap();
|
||||
let parsed = parse_patch(&patch, ParsePatchMode::Legacy).unwrap();
|
||||
let chunks = match parsed.hunks.as_slice() {
|
||||
[Hunk::UpdateFile { chunks, .. }] => chunks,
|
||||
_ => panic!("Expected a single UpdateFile hunk"),
|
||||
@@ -1281,6 +1280,7 @@ mod tests {
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(
|
||||
&patch,
|
||||
ParsePatchMode::Legacy,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
@@ -1321,6 +1321,7 @@ g
|
||||
let mut stderr = Vec::new();
|
||||
let result = apply_patch(
|
||||
&patch,
|
||||
ParsePatchMode::Legacy,
|
||||
&AbsolutePathBuf::from_absolute_path(dir.path()).unwrap(),
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
|
||||
@@ -129,16 +129,17 @@ pub enum ParsePatchMode {
|
||||
Streaming,
|
||||
}
|
||||
|
||||
pub fn parse_patch(patch: &str) -> Result<ApplyPatchArgs, ParseError> {
|
||||
parse_patch_with_mode(patch, ParsePatchMode::Legacy)
|
||||
}
|
||||
|
||||
pub fn parse_patch_with_mode(
|
||||
patch: &str,
|
||||
mode: ParsePatchMode,
|
||||
) -> Result<ApplyPatchArgs, ParseError> {
|
||||
pub fn parse_patch(patch: &str, mode: ParsePatchMode) -> Result<ApplyPatchArgs, ParseError> {
|
||||
if mode == ParsePatchMode::Streaming {
|
||||
return crate::streaming_parser::parse_patch(patch);
|
||||
let patch = normalize_patch_text(patch)?;
|
||||
let mut parser = crate::streaming_parser::StreamingPatchParser::default();
|
||||
parser.push_delta(&patch)?;
|
||||
let hunks = parser.finish()?;
|
||||
return Ok(ApplyPatchArgs {
|
||||
hunks,
|
||||
patch,
|
||||
workdir: None,
|
||||
});
|
||||
}
|
||||
|
||||
let mode = if PARSE_IN_STRICT_MODE {
|
||||
@@ -149,6 +150,12 @@ pub fn parse_patch_with_mode(
|
||||
parse_patch_text(patch, mode)
|
||||
}
|
||||
|
||||
fn normalize_patch_text(patch: &str) -> Result<String, ParseError> {
|
||||
let lines: Vec<&str> = patch.trim().lines().collect();
|
||||
let (patch_lines, _) = check_patch_boundaries_lenient(&lines)?;
|
||||
Ok(patch_lines.join("\n"))
|
||||
}
|
||||
|
||||
enum ParseMode {
|
||||
/// Parse the patch text argument as is.
|
||||
Strict,
|
||||
|
||||
@@ -67,6 +67,7 @@ pub fn run_main() -> i32 {
|
||||
};
|
||||
match runtime.block_on(crate::apply_patch(
|
||||
&patch_arg,
|
||||
crate::ParsePatchMode::Legacy,
|
||||
&cwd,
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::ApplyPatchArgs;
|
||||
use crate::parser::ADD_FILE_MARKER;
|
||||
use crate::parser::BEGIN_PATCH_MARKER;
|
||||
use crate::parser::CHANGE_CONTEXT_MARKER;
|
||||
@@ -17,67 +16,6 @@ use crate::parser::UpdateFileChunk;
|
||||
use Hunk::*;
|
||||
use ParseError::*;
|
||||
|
||||
pub fn parse_patch(patch: &str) -> Result<ApplyPatchArgs, ParseError> {
|
||||
let patch = normalize_patch_text(patch)?;
|
||||
let mut parser = StreamingPatchParser::default();
|
||||
parser.push_delta(&patch)?;
|
||||
let hunks = parser.finish()?;
|
||||
Ok(ApplyPatchArgs {
|
||||
patch,
|
||||
hunks,
|
||||
workdir: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_patch_text(patch: &str) -> Result<String, ParseError> {
|
||||
let lines: Vec<&str> = patch.trim().lines().collect();
|
||||
let patch_lines = match check_patch_boundaries(&lines) {
|
||||
Ok(lines) => lines,
|
||||
Err(original_parse_error) => match lines.as_slice() {
|
||||
[first, .., last]
|
||||
if matches!(*first, "<<EOF" | "<<'EOF'" | "<<\"EOF\"")
|
||||
&& last.ends_with("EOF")
|
||||
&& lines.len() >= 4 =>
|
||||
{
|
||||
let inner_lines = &lines[1..lines.len() - 1];
|
||||
check_patch_boundaries(inner_lines)?
|
||||
}
|
||||
_ => return Err(original_parse_error),
|
||||
},
|
||||
};
|
||||
Ok(patch_lines.join("\n"))
|
||||
}
|
||||
|
||||
fn check_patch_boundaries<'a>(lines: &'a [&'a str]) -> Result<&'a [&'a str], ParseError> {
|
||||
let (first_line, last_line) = match lines {
|
||||
[] => (None, None),
|
||||
[first] => (Some(first), Some(first)),
|
||||
[first, .., last] => (Some(first), Some(last)),
|
||||
};
|
||||
check_start_and_end_lines(first_line, last_line)?;
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
fn check_start_and_end_lines(
|
||||
first_line: Option<&&str>,
|
||||
last_line: Option<&&str>,
|
||||
) -> Result<(), ParseError> {
|
||||
let first_line = first_line.map(|line| line.trim());
|
||||
let last_line = last_line.map(|line| line.trim());
|
||||
|
||||
match (first_line, last_line) {
|
||||
(Some(first), Some(last)) if first == BEGIN_PATCH_MARKER && last == END_PATCH_MARKER => {
|
||||
Ok(())
|
||||
}
|
||||
(Some(first), _) if first != BEGIN_PATCH_MARKER => Err(InvalidPatchError(String::from(
|
||||
"The first line of the patch must be '*** Begin Patch'",
|
||||
))),
|
||||
_ => Err(InvalidPatchError(String::from(
|
||||
"The last line of the patch must be '*** End Patch'",
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct StreamingPatchParser {
|
||||
line_buffer: String,
|
||||
@@ -428,36 +366,6 @@ mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::*;
|
||||
use crate::parser;
|
||||
|
||||
#[test]
|
||||
fn test_parse_patch_matches_legacy_parser_for_complete_input() {
|
||||
let patch = "\
|
||||
*** Begin Patch
|
||||
*** Add File: added.txt
|
||||
+hello
|
||||
*** Update File: changed.txt
|
||||
@@
|
||||
-old
|
||||
+new
|
||||
*** Delete File: removed.txt
|
||||
*** End Patch";
|
||||
|
||||
assert_eq!(parse_patch(patch), parser::parse_patch(patch));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_patch_supports_lenient_heredoc_wrapper() {
|
||||
let patch = "\
|
||||
<<'EOF'
|
||||
*** Begin Patch
|
||||
*** Add File: added.txt
|
||||
+hello
|
||||
*** End Patch
|
||||
EOF";
|
||||
|
||||
assert_eq!(parse_patch(patch), parser::parse_patch(patch));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_streaming_patch_parser_streams_complete_lines_before_end_patch() {
|
||||
|
||||
@@ -116,6 +116,7 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
|
||||
};
|
||||
match runtime.block_on(codex_apply_patch::apply_patch(
|
||||
&patch_arg,
|
||||
codex_apply_patch::ParsePatchMode::Legacy,
|
||||
&cwd,
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
|
||||
@@ -446,7 +446,7 @@ struct AppServerCommand {
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct ExecServerCommand {
|
||||
/// Transport endpoint URL. Supported values: `ws://IP:PORT` (default).
|
||||
/// Transport endpoint URL. Supported values: `ws://IP:PORT` (default), `stdio`, `stdio://`.
|
||||
#[arg(
|
||||
long = "listen",
|
||||
value_name = "URL",
|
||||
@@ -1388,7 +1388,7 @@ async fn run_debug_prompt_input_command(
|
||||
});
|
||||
}
|
||||
|
||||
let prompt_input = codex_core::build_prompt_input(config, input).await?;
|
||||
let prompt_input = codex_core::build_prompt_input(config, input, /*state_db*/ None).await?;
|
||||
println!("{}", serde_json::to_string_pretty(&prompt_input)?);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -104,6 +104,8 @@ pub struct TuiGlobalKeymap {
|
||||
pub toggle_shortcuts: Option<KeybindingsSpec>,
|
||||
/// Toggle Vim mode for the composer input.
|
||||
pub toggle_vim_mode: Option<KeybindingsSpec>,
|
||||
/// Toggle Fast mode.
|
||||
pub toggle_fast_mode: Option<KeybindingsSpec>,
|
||||
}
|
||||
|
||||
/// Chat context keybindings.
|
||||
@@ -169,6 +171,8 @@ pub struct TuiEditorKeymap {
|
||||
pub delete_forward_word: Option<KeybindingsSpec>,
|
||||
/// Kill text from cursor to line start.
|
||||
pub kill_line_start: Option<KeybindingsSpec>,
|
||||
/// Kill the current line.
|
||||
pub kill_whole_line: Option<KeybindingsSpec>,
|
||||
/// Kill text from cursor to line end.
|
||||
pub kill_line_end: Option<KeybindingsSpec>,
|
||||
/// Yank the kill buffer.
|
||||
|
||||
@@ -27,6 +27,7 @@ pub use codex_core::ForkSnapshot;
|
||||
pub use codex_core::McpManager;
|
||||
pub use codex_core::NewThread;
|
||||
pub use codex_core::StartThreadOptions;
|
||||
pub use codex_core::StateDbHandle;
|
||||
pub use codex_core::ThreadManager;
|
||||
pub use codex_core::ThreadShutdownReport;
|
||||
pub use codex_core::config::Config;
|
||||
@@ -37,6 +38,7 @@ pub use codex_core::config::Permissions;
|
||||
pub use codex_core::config::TerminalResizeReflowConfig;
|
||||
pub use codex_core::config::ThreadStoreConfig;
|
||||
pub use codex_core::config::find_codex_home;
|
||||
pub use codex_core::init_state_db;
|
||||
pub use codex_core::skills::SkillsManager;
|
||||
pub use codex_core::thread_store_from_config;
|
||||
pub use codex_exec_server::EnvironmentManager;
|
||||
|
||||
@@ -2444,6 +2444,7 @@
|
||||
"insert_newline": null,
|
||||
"kill_line_end": null,
|
||||
"kill_line_start": null,
|
||||
"kill_whole_line": null,
|
||||
"move_down": null,
|
||||
"move_left": null,
|
||||
"move_line_end": null,
|
||||
@@ -2461,6 +2462,7 @@
|
||||
"open_transcript": null,
|
||||
"queue": null,
|
||||
"submit": null,
|
||||
"toggle_fast_mode": null,
|
||||
"toggle_shortcuts": null,
|
||||
"toggle_vim_mode": null
|
||||
},
|
||||
@@ -2814,6 +2816,14 @@
|
||||
],
|
||||
"description": "Kill text from cursor to line start."
|
||||
},
|
||||
"kill_whole_line": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Kill the current line."
|
||||
},
|
||||
"move_down": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2941,6 +2951,14 @@
|
||||
],
|
||||
"description": "Submit the current composer draft."
|
||||
},
|
||||
"toggle_fast_mode": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Toggle Fast mode."
|
||||
},
|
||||
"toggle_shortcuts": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -3021,6 +3039,7 @@
|
||||
"insert_newline": null,
|
||||
"kill_line_end": null,
|
||||
"kill_line_start": null,
|
||||
"kill_whole_line": null,
|
||||
"move_down": null,
|
||||
"move_left": null,
|
||||
"move_line_end": null,
|
||||
@@ -3045,6 +3064,7 @@
|
||||
"open_transcript": null,
|
||||
"queue": null,
|
||||
"submit": null,
|
||||
"toggle_fast_mode": null,
|
||||
"toggle_shortcuts": null,
|
||||
"toggle_vim_mode": null
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_rollout::state_db;
|
||||
use codex_state::DirectionalThreadSpawnEdgeStatus;
|
||||
use codex_thread_store::ReadThreadParams;
|
||||
use serde::Serialize;
|
||||
@@ -526,6 +525,7 @@ impl AgentControl {
|
||||
let _ = config.features.disable(Feature::Collab);
|
||||
}
|
||||
let state = self.upgrade()?;
|
||||
let state_db_ctx = state.state_db();
|
||||
let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
|
||||
let (session_source, agent_metadata) = match session_source {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
@@ -536,7 +536,7 @@ impl AgentControl {
|
||||
agent_nickname: _,
|
||||
}) => {
|
||||
let (resumed_agent_nickname, resumed_agent_role) =
|
||||
if let Some(state_db_ctx) = state_db::get_state_db(&config).await {
|
||||
if let Some(state_db_ctx) = state_db_ctx.as_ref() {
|
||||
match state_db_ctx.get_thread(thread_id).await {
|
||||
Ok(Some(metadata)) => (metadata.agent_nickname, metadata.agent_role),
|
||||
Ok(None) | Err(_) => (None, None),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use crate::CodexThread;
|
||||
use crate::StateDbHandle;
|
||||
use crate::ThreadManager;
|
||||
use crate::agent::agent_status_from_event;
|
||||
use crate::config::AgentRoleConfig;
|
||||
@@ -7,6 +8,7 @@ use crate::config::Config;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::SubagentNotification;
|
||||
use crate::init_state_db;
|
||||
use assert_matches::assert_matches;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -84,6 +86,7 @@ fn spawn_agent_call(call_id: &str) -> ResponseItem {
|
||||
struct AgentControlHarness {
|
||||
_home: TempDir,
|
||||
config: Config,
|
||||
state_db: Option<StateDbHandle>,
|
||||
manager: ThreadManager,
|
||||
control: AgentControl,
|
||||
}
|
||||
@@ -91,16 +94,19 @@ struct AgentControlHarness {
|
||||
impl AgentControlHarness {
|
||||
async fn new() -> Self {
|
||||
let (home, config) = test_config().await;
|
||||
let manager = ThreadManager::with_models_provider_and_home_for_tests(
|
||||
let state_db = init_state_db(&config).await;
|
||||
let manager = ThreadManager::with_models_provider_home_and_state_for_tests(
|
||||
CodexAuth::from_api_key("dummy"),
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
state_db.clone(),
|
||||
);
|
||||
let control = manager.agent_control();
|
||||
Self {
|
||||
_home: home,
|
||||
config,
|
||||
state_db,
|
||||
manager,
|
||||
control,
|
||||
}
|
||||
@@ -1537,16 +1543,19 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
|
||||
.features
|
||||
.enable(Feature::Sqlite)
|
||||
.expect("test config should allow sqlite");
|
||||
let manager = ThreadManager::with_models_provider_and_home_for_tests(
|
||||
let state_db = init_state_db(&config).await;
|
||||
let manager = ThreadManager::with_models_provider_home_and_state_for_tests(
|
||||
CodexAuth::from_api_key("dummy"),
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
state_db.clone(),
|
||||
);
|
||||
let control = manager.agent_control();
|
||||
let harness = AgentControlHarness {
|
||||
_home: home,
|
||||
config,
|
||||
state_db,
|
||||
manager,
|
||||
control,
|
||||
};
|
||||
@@ -1695,7 +1704,10 @@ async fn resume_agent_from_rollout_reads_archived_rollout_path() {
|
||||
.shutdown_live_agent(child_thread_id)
|
||||
.await
|
||||
.expect("child shutdown should succeed");
|
||||
let store = LocalThreadStore::new(LocalThreadStoreConfig::from_config(&harness.config));
|
||||
let store = LocalThreadStore::new(
|
||||
LocalThreadStoreConfig::from_config(&harness.config),
|
||||
harness.state_db.clone(),
|
||||
);
|
||||
store
|
||||
.archive_thread(ArchiveThreadParams {
|
||||
thread_id: child_thread_id,
|
||||
|
||||
@@ -30,7 +30,6 @@ use tokio::time::timeout;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::environment_selection::ResolvedTurnEnvironments;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
@@ -94,9 +93,7 @@ pub(crate) async fn run_codex_thread_interactive(
|
||||
inherited_exec_policy: Some(Arc::clone(&parent_session.services.exec_policy)),
|
||||
parent_rollout_thread_trace: codex_rollout_trace::ThreadTraceContext::disabled(),
|
||||
parent_trace: None,
|
||||
environment_selections: ResolvedTurnEnvironments {
|
||||
turn_environments: parent_ctx.environments.clone(),
|
||||
},
|
||||
environment_selections: parent_ctx.environments.clone(),
|
||||
analytics_events_client: Some(parent_session.services.analytics_events_client.clone()),
|
||||
thread_store: Arc::clone(&parent_session.services.thread_store),
|
||||
}))
|
||||
|
||||
@@ -176,7 +176,9 @@ impl EnvironmentContext {
|
||||
|
||||
pub(crate) fn from_turn_context(turn_context: &TurnContext) -> Self {
|
||||
Self::new(
|
||||
EnvironmentContextEnvironment::from_turn_environments(&turn_context.environments),
|
||||
EnvironmentContextEnvironment::from_turn_environments(
|
||||
&turn_context.environments.turn_environments,
|
||||
),
|
||||
turn_context.current_date.clone(),
|
||||
turn_context.timezone.clone(),
|
||||
Self::network_from_turn_context(turn_context),
|
||||
|
||||
@@ -24,7 +24,7 @@ pub(crate) fn default_thread_environment_selections(
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct ResolvedTurnEnvironments {
|
||||
pub(crate) turn_environments: Vec<TurnEnvironment>,
|
||||
}
|
||||
@@ -37,17 +37,17 @@ impl ResolvedTurnEnvironments {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn primary_turn_environment(&self) -> Option<&TurnEnvironment> {
|
||||
pub(crate) fn primary(&self) -> Option<&TurnEnvironment> {
|
||||
self.turn_environments.first()
|
||||
}
|
||||
|
||||
pub(crate) fn primary_environment(&self) -> Option<Arc<codex_exec_server::Environment>> {
|
||||
self.primary_turn_environment()
|
||||
self.primary()
|
||||
.map(|environment| Arc::clone(&environment.environment))
|
||||
}
|
||||
|
||||
pub(crate) fn primary_filesystem(&self) -> Option<Arc<dyn ExecutorFileSystem>> {
|
||||
self.primary_turn_environment()
|
||||
self.primary()
|
||||
.map(|environment| environment.environment.get_filesystem())
|
||||
}
|
||||
}
|
||||
@@ -171,7 +171,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
resolved
|
||||
.primary_turn_environment()
|
||||
.primary()
|
||||
.expect("primary environment")
|
||||
.environment_id,
|
||||
"local"
|
||||
|
||||
@@ -145,7 +145,7 @@ pub(crate) mod shell_snapshot;
|
||||
pub mod spawn;
|
||||
pub(crate) mod state_db_bridge;
|
||||
pub use state_db_bridge::StateDbHandle;
|
||||
pub use state_db_bridge::get_state_db;
|
||||
pub use state_db_bridge::init_state_db;
|
||||
mod thread_rollout_truncation;
|
||||
mod tools;
|
||||
pub(crate) mod turn_diff_tracker;
|
||||
|
||||
@@ -1,6 +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_thread_store::ListThreadsParams;
|
||||
use codex_thread_store::LocalThreadStore;
|
||||
use codex_thread_store::LocalThreadStoreConfig;
|
||||
@@ -24,6 +25,7 @@ pub enum PersonalityMigrationStatus {
|
||||
pub async fn maybe_migrate_personality(
|
||||
codex_home: &Path,
|
||||
config_toml: &ConfigToml,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) -> io::Result<PersonalityMigrationStatus> {
|
||||
let marker_path = codex_home.join(PERSONALITY_MIGRATION_FILENAME);
|
||||
if tokio::fs::try_exists(&marker_path).await? {
|
||||
@@ -43,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()).await? {
|
||||
if !has_recorded_sessions(codex_home, model_provider_id.as_str(), state_db).await? {
|
||||
create_marker(&marker_path).await?;
|
||||
return Ok(PersonalityMigrationStatus::SkippedNoSessions);
|
||||
}
|
||||
@@ -60,12 +62,19 @@ pub async fn maybe_migrate_personality(
|
||||
Ok(PersonalityMigrationStatus::Applied)
|
||||
}
|
||||
|
||||
async fn has_recorded_sessions(codex_home: &Path, default_provider: &str) -> io::Result<bool> {
|
||||
let store = LocalThreadStore::new(LocalThreadStoreConfig {
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
sqlite_home: codex_home.to_path_buf(),
|
||||
default_model_provider_id: default_provider.to_string(),
|
||||
});
|
||||
async fn has_recorded_sessions(
|
||||
codex_home: &Path,
|
||||
default_provider: &str,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) -> io::Result<bool> {
|
||||
let store = LocalThreadStore::new(
|
||||
LocalThreadStoreConfig {
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
sqlite_home: codex_home.to_path_buf(),
|
||||
default_model_provider_id: default_provider.to_string(),
|
||||
},
|
||||
state_db,
|
||||
);
|
||||
if has_threads(&store, /*archived*/ false).await? {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ 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).await?;
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(status, PersonalityMigrationStatus::Applied);
|
||||
assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists());
|
||||
@@ -103,7 +103,7 @@ 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).await?;
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(status, PersonalityMigrationStatus::Applied);
|
||||
assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists());
|
||||
@@ -119,7 +119,7 @@ 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).await?;
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(status, PersonalityMigrationStatus::SkippedMarker);
|
||||
assert!(!temp.path().join("config.toml").exists());
|
||||
@@ -136,7 +136,7 @@ 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).await?;
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
@@ -153,7 +153,7 @@ 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).await?;
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(status, PersonalityMigrationStatus::SkippedNoSessions);
|
||||
assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists());
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::config::Config;
|
||||
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::thread_manager::ThreadManager;
|
||||
use crate::thread_manager::thread_store_from_config;
|
||||
|
||||
@@ -24,6 +25,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>,
|
||||
) -> CodexResult<Vec<ResponseItem>> {
|
||||
config.ephemeral = true;
|
||||
|
||||
@@ -35,13 +37,15 @@ 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_manager = ThreadManager::new(
|
||||
&config,
|
||||
Arc::clone(&auth_manager),
|
||||
SessionSource::Exec,
|
||||
Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::new(local_runtime_paths)).await),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config),
|
||||
thread_store,
|
||||
state_db.clone(),
|
||||
);
|
||||
let thread = thread_manager.start_thread(config).await?;
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ impl Session {
|
||||
let mcp_servers = with_codex_apps_mcp(mcp_servers, auth.as_ref(), &mcp_config);
|
||||
let auth_statuses =
|
||||
compute_auth_statuses(mcp_servers.iter(), store_mode, auth.as_ref()).await;
|
||||
let mcp_runtime_environment = match turn_context.primary_environment() {
|
||||
let mcp_runtime_environment = match turn_context.environments.primary() {
|
||||
Some(turn_environment) => McpRuntimeEnvironment::new(
|
||||
Arc::clone(&turn_environment.environment),
|
||||
turn_environment.cwd.to_path_buf(),
|
||||
|
||||
@@ -355,6 +355,7 @@ use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_tools::ToolEnvironmentMode;
|
||||
use codex_tools::ToolsConfig;
|
||||
use codex_tools::ToolsConfigParams;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -555,7 +556,15 @@ impl Codex {
|
||||
};
|
||||
match thread_id {
|
||||
Some(thread_id) => {
|
||||
let state_db_ctx = state_db::get_state_db(&config).await;
|
||||
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::get_dynamic_tools(state_db_ctx.as_deref(), thread_id, "codex_spawn")
|
||||
.await
|
||||
}
|
||||
@@ -1297,6 +1306,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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ pub(super) async fn spawn_review_thread(
|
||||
)
|
||||
.with_web_search_config(/*web_search_config*/ None)
|
||||
.with_allow_login_shell(config.permissions.allow_login_shell)
|
||||
.with_has_environment(parent_turn_context.tools_config.has_environment)
|
||||
.with_environment_mode(parent_turn_context.tools_config.environment_mode)
|
||||
.with_spawn_agent_usage_hint(config.multi_agent_v2.usage_hint_enabled)
|
||||
.with_spawn_agent_usage_hint_text(config.multi_agent_v2.usage_hint_text.clone())
|
||||
.with_hide_spawn_agent_metadata(config.multi_agent_v2.hide_spawn_agent_metadata)
|
||||
|
||||
@@ -726,6 +726,7 @@ impl Session {
|
||||
session_configuration.cwd.clone(),
|
||||
&mut default_shell,
|
||||
session_telemetry.clone(),
|
||||
state_db_ctx.clone(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -968,7 +969,7 @@ impl Session {
|
||||
"unknown stored MCP environment id",
|
||||
))
|
||||
})?
|
||||
.primary_turn_environment()
|
||||
.primary()
|
||||
.cloned();
|
||||
let mcp_runtime_environment = match turn_environment {
|
||||
Some(turn_environment) => McpRuntimeEnvironment::new(
|
||||
|
||||
@@ -2944,13 +2944,15 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
fn turn_environments_for_tests(
|
||||
environment: &Arc<codex_exec_server::Environment>,
|
||||
cwd: &codex_utils_absolute_path::AbsolutePathBuf,
|
||||
) -> Vec<TurnEnvironment> {
|
||||
vec![TurnEnvironment {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
environment: Arc::clone(environment),
|
||||
cwd: cwd.clone(),
|
||||
shell: "bash".to_string(),
|
||||
}]
|
||||
) -> crate::environment_selection::ResolvedTurnEnvironments {
|
||||
crate::environment_selection::ResolvedTurnEnvironments {
|
||||
turn_environments: vec![TurnEnvironment {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
environment: Arc::clone(environment),
|
||||
cwd: cwd.clone(),
|
||||
shell: "bash".to_string(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -3399,7 +3401,7 @@ async fn absolute_cwd_update_with_turn_environment_is_allowed() {
|
||||
|
||||
assert_eq!(turn_context.cwd, absolute_cwd);
|
||||
assert_eq!(turn_context.config.cwd, absolute_cwd);
|
||||
assert_eq!(turn_context.environments.len(), 1);
|
||||
assert_eq!(turn_context.environments.turn_environments.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -3490,6 +3492,7 @@ 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,
|
||||
)),
|
||||
codex_rollout_trace::ThreadTraceContext::disabled(),
|
||||
)
|
||||
@@ -3637,6 +3640,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
live_thread: None,
|
||||
thread_store: Arc::new(codex_thread_store::LocalThreadStore::new(
|
||||
codex_thread_store::LocalThreadStoreConfig::from_config(config.as_ref()),
|
||||
/*state_db*/ None,
|
||||
)),
|
||||
model_client: ModelClient::new(
|
||||
Some(auth_manager.clone()),
|
||||
@@ -3808,6 +3812,7 @@ 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,
|
||||
)),
|
||||
codex_rollout_trace::ThreadTraceContext::disabled(),
|
||||
)
|
||||
@@ -4418,15 +4423,16 @@ async fn turn_environments_set_primary_environment() {
|
||||
.expect("turn should start");
|
||||
|
||||
let turn_environments = &turn_context.environments;
|
||||
assert_eq!(turn_environments.len(), 1);
|
||||
assert_eq!(turn_environments.turn_environments.len(), 1);
|
||||
let turn_environment = turn_context
|
||||
.primary_environment()
|
||||
.environments
|
||||
.primary()
|
||||
.expect("primary environment should be set");
|
||||
assert!(std::sync::Arc::ptr_eq(
|
||||
&turn_environment.environment,
|
||||
&turn_environments[0].environment
|
||||
&turn_environments.turn_environments[0].environment
|
||||
));
|
||||
assert!(!turn_context.environments.is_empty());
|
||||
assert!(!turn_context.environments.turn_environments.is_empty());
|
||||
assert_eq!(turn_context.cwd.as_path(), selected_cwd.as_path());
|
||||
assert_eq!(turn_context.config.cwd.as_path(), selected_cwd.as_path());
|
||||
}
|
||||
@@ -4449,13 +4455,14 @@ async fn default_turn_overlays_session_cwd_onto_stored_thread_environments() {
|
||||
let turn_context = session.new_default_turn().await;
|
||||
|
||||
let turn_environments = &turn_context.environments;
|
||||
assert_eq!(turn_environments.len(), 1);
|
||||
assert_eq!(turn_environments.turn_environments.len(), 1);
|
||||
let turn_environment = turn_context
|
||||
.primary_environment()
|
||||
.environments
|
||||
.primary()
|
||||
.expect("primary environment should be set");
|
||||
assert!(std::sync::Arc::ptr_eq(
|
||||
&turn_environment.environment,
|
||||
&turn_environments[0].environment
|
||||
&turn_environments.turn_environments[0].environment
|
||||
));
|
||||
assert_eq!(turn_context.cwd, session_cwd);
|
||||
assert_eq!(turn_context.config.cwd, session_cwd);
|
||||
@@ -4473,28 +4480,32 @@ async fn default_turn_honors_empty_stored_thread_environments() {
|
||||
|
||||
let turn_context = session.new_default_turn().await;
|
||||
|
||||
assert!(turn_context.primary_environment().is_none());
|
||||
assert!(turn_context.environments.is_empty());
|
||||
assert!(turn_context.environments.primary().is_none());
|
||||
assert!(turn_context.environments.turn_environments.is_empty());
|
||||
assert_eq!(turn_context.cwd, session_cwd);
|
||||
assert_eq!(turn_context.config.cwd, session_cwd);
|
||||
assert_eq!(turn_context.environments.len(), 0);
|
||||
assert_eq!(turn_context.environments.turn_environments.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn primary_environment_uses_first_turn_environment() {
|
||||
let (_session, mut turn_context) = make_session_and_context().await;
|
||||
let first_environment = turn_context.environments[0].clone();
|
||||
let first_environment = turn_context.environments.turn_environments[0].clone();
|
||||
let second_cwd = turn_context.cwd.join("second");
|
||||
turn_context.environments.push(TurnEnvironment {
|
||||
environment_id: "second".to_string(),
|
||||
environment: Arc::clone(&first_environment.environment),
|
||||
cwd: second_cwd.clone(),
|
||||
shell: first_environment.shell.clone(),
|
||||
});
|
||||
turn_context
|
||||
.environments
|
||||
.turn_environments
|
||||
.push(TurnEnvironment {
|
||||
environment_id: "second".to_string(),
|
||||
environment: Arc::clone(&first_environment.environment),
|
||||
cwd: second_cwd.clone(),
|
||||
shell: first_environment.shell.clone(),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
turn_context
|
||||
.primary_environment()
|
||||
.environments
|
||||
.primary()
|
||||
.expect("primary environment")
|
||||
.environment_id,
|
||||
first_environment.environment_id
|
||||
@@ -4502,14 +4513,18 @@ async fn primary_environment_uses_first_turn_environment() {
|
||||
assert_eq!(
|
||||
turn_context
|
||||
.environments
|
||||
.turn_environments
|
||||
.iter()
|
||||
.find(|environment| environment.environment_id == "second")
|
||||
.expect("second environment")
|
||||
.cwd,
|
||||
second_cwd
|
||||
);
|
||||
assert_eq!(turn_context.environments.len(), 2);
|
||||
assert_eq!(turn_context.environments[1].cwd, second_cwd);
|
||||
assert_eq!(turn_context.environments.turn_environments.len(), 2);
|
||||
assert_eq!(
|
||||
turn_context.environments.turn_environments[1].cwd,
|
||||
second_cwd
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -4527,8 +4542,8 @@ async fn empty_turn_environments_clear_primary_environment() {
|
||||
.await
|
||||
.expect("turn should start");
|
||||
|
||||
assert!(turn_context.primary_environment().is_none());
|
||||
assert!(turn_context.environments.is_empty());
|
||||
assert!(turn_context.environments.primary().is_none());
|
||||
assert!(turn_context.environments.turn_environments.is_empty());
|
||||
assert_eq!(turn_context.cwd, session.get_config().await.cwd);
|
||||
assert_eq!(turn_context.config.cwd, session.get_config().await.cwd);
|
||||
}
|
||||
@@ -4986,10 +5001,44 @@ async fn make_session_and_context_with_auth_and_config_and_rx<F>(
|
||||
where
|
||||
F: FnOnce(&mut Config),
|
||||
{
|
||||
let (tx_event, rx_event) = async_channel::unbounded();
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
let mut config = build_test_config(codex_home.path()).await;
|
||||
make_session_and_context_with_auth_config_home_and_rx(
|
||||
auth,
|
||||
dynamic_tools,
|
||||
codex_home.path(),
|
||||
configure_config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn make_session_and_context_with_auth_config_home_and_rx<F>(
|
||||
auth: CodexAuth,
|
||||
dynamic_tools: Vec<DynamicToolSpec>,
|
||||
codex_home: &Path,
|
||||
configure_config: F,
|
||||
) -> (
|
||||
Arc<Session>,
|
||||
Arc<TurnContext>,
|
||||
async_channel::Receiver<Event>,
|
||||
)
|
||||
where
|
||||
F: FnOnce(&mut Config),
|
||||
{
|
||||
let (tx_event, rx_event) = async_channel::unbounded();
|
||||
let mut config = build_test_config(codex_home).await;
|
||||
configure_config(&mut config);
|
||||
let state_db = if config.features.enabled(Feature::Goals) {
|
||||
Some(
|
||||
codex_state::StateRuntime::init(
|
||||
config.sqlite_home.clone(),
|
||||
config.model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("goal tests should initialize sqlite state db"),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let config = Arc::new(config);
|
||||
let conversation_id = ThreadId::default();
|
||||
let auth_manager = AuthManager::from_auth_for_testing(auth);
|
||||
@@ -5115,10 +5164,11 @@ where
|
||||
agent_control,
|
||||
network_proxy: None,
|
||||
network_approval: Arc::clone(&network_approval),
|
||||
state_db: None,
|
||||
state_db: state_db.clone(),
|
||||
live_thread: None,
|
||||
thread_store: Arc::new(codex_thread_store::LocalThreadStore::new(
|
||||
codex_thread_store::LocalThreadStoreConfig::from_config(config.as_ref()),
|
||||
state_db,
|
||||
)),
|
||||
model_client: ModelClient::new(
|
||||
Some(Arc::clone(&auth_manager)),
|
||||
@@ -5213,10 +5263,13 @@ async fn make_goal_session_and_context_with_rx() -> (
|
||||
Arc<Session>,
|
||||
Arc<TurnContext>,
|
||||
async_channel::Receiver<Event>,
|
||||
tempfile::TempDir,
|
||||
) {
|
||||
let (session, turn_context, rx) = make_session_and_context_with_auth_and_config_and_rx(
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
let (session, turn_context, rx) = make_session_and_context_with_auth_config_home_and_rx(
|
||||
CodexAuth::from_api_key("Test API Key"),
|
||||
Vec::new(),
|
||||
codex_home.path(),
|
||||
|config| {
|
||||
config
|
||||
.features
|
||||
@@ -5226,14 +5279,14 @@ async fn make_goal_session_and_context_with_rx() -> (
|
||||
)
|
||||
.await;
|
||||
upsert_goal_test_thread(session.as_ref()).await;
|
||||
(session, turn_context, rx)
|
||||
(session, turn_context, rx, codex_home)
|
||||
}
|
||||
|
||||
async fn upsert_goal_test_thread(session: &Session) {
|
||||
let config = session.get_config().await;
|
||||
let state_db = goal_test_state_db(session)
|
||||
.await
|
||||
.expect("goal test state db should initialize");
|
||||
let state_db = session
|
||||
.state_db()
|
||||
.expect("goal test session should have a state db");
|
||||
let mut builder = codex_state::ThreadMetadataBuilder::new(
|
||||
session.conversation_id,
|
||||
config
|
||||
@@ -7050,7 +7103,7 @@ async fn abort_empty_active_turn_preserves_pending_input() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn interrupt_accounts_active_goal_before_pausing() -> anyhow::Result<()> {
|
||||
let (sess, tc, _rx) = make_goal_session_and_context_with_rx().await;
|
||||
let (sess, tc, _rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
sess.set_thread_goal(
|
||||
tc.as_ref(),
|
||||
SetGoalRequest {
|
||||
@@ -7304,6 +7357,9 @@ fn post_goal_token_usage() -> TokenUsage {
|
||||
}
|
||||
|
||||
async fn goal_test_state_db(sess: &Session) -> anyhow::Result<crate::StateDbHandle> {
|
||||
if let Some(state_db) = sess.state_db() {
|
||||
return Ok(state_db);
|
||||
}
|
||||
let config = sess.get_config().await;
|
||||
codex_state::StateRuntime::init(config.sqlite_home.clone(), config.model_provider_id.clone())
|
||||
.await
|
||||
@@ -7311,7 +7367,7 @@ async fn goal_test_state_db(sess: &Session) -> anyhow::Result<crate::StateDbHand
|
||||
|
||||
#[tokio::test]
|
||||
async fn budget_limited_accounting_steers_active_turn_without_aborting() -> anyhow::Result<()> {
|
||||
let (sess, tc, rx) = make_goal_session_and_context_with_rx().await;
|
||||
let (sess, tc, rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
sess.set_thread_goal(
|
||||
tc.as_ref(),
|
||||
SetGoalRequest {
|
||||
@@ -7411,7 +7467,7 @@ async fn budget_limited_accounting_steers_active_turn_without_aborting() -> anyh
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn external_goal_mutation_accounts_active_turn_before_status_change() -> anyhow::Result<()> {
|
||||
let (sess, tc, _rx) = make_goal_session_and_context_with_rx().await;
|
||||
let (sess, tc, _rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
sess.set_thread_goal(
|
||||
tc.as_ref(),
|
||||
SetGoalRequest {
|
||||
@@ -7473,7 +7529,7 @@ async fn external_goal_mutation_accounts_active_turn_before_status_change() -> a
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn external_active_goal_set_marks_current_turn_for_accounting() -> anyhow::Result<()> {
|
||||
let (sess, tc, _rx) = make_goal_session_and_context_with_rx().await;
|
||||
let (sess, tc, _rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
Vec::new(),
|
||||
@@ -8137,7 +8193,7 @@ async fn sample_rollout(
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_goal_tool_rejects_existing_goal() {
|
||||
let (session, turn_context, _rx) = make_goal_session_and_context_with_rx().await;
|
||||
let (session, turn_context, _rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
|
||||
let handler = GoalHandler;
|
||||
|
||||
@@ -8199,7 +8255,7 @@ async fn create_goal_tool_rejects_existing_goal() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_goal_tool_rejects_pausing_goal() {
|
||||
let (session, turn_context, _rx) = make_goal_session_and_context_with_rx().await;
|
||||
let (session, turn_context, _rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
|
||||
let handler = GoalHandler;
|
||||
|
||||
@@ -8259,7 +8315,7 @@ async fn update_goal_tool_rejects_pausing_goal() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_goal_tool_marks_goal_complete() {
|
||||
let (session, turn_context, _rx) = make_goal_session_and_context_with_rx().await;
|
||||
let (session, turn_context, _rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
|
||||
let handler = GoalHandler;
|
||||
|
||||
|
||||
@@ -731,6 +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,
|
||||
));
|
||||
|
||||
let CodexSpawnOk { codex, .. } = Codex::spawn(CodexSpawnArgs {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use crate::config::GhostSnapshotConfig;
|
||||
use crate::environment_selection::ResolvedTurnEnvironments;
|
||||
use codex_model_provider::SharedModelProvider;
|
||||
use codex_model_provider::create_model_provider;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
@@ -60,7 +61,7 @@ pub(crate) struct TurnContext {
|
||||
pub(crate) reasoning_effort: Option<ReasoningEffortConfig>,
|
||||
pub(crate) reasoning_summary: ReasoningSummaryConfig,
|
||||
pub(crate) session_source: SessionSource,
|
||||
pub(crate) environments: Vec<TurnEnvironment>,
|
||||
pub(crate) environments: ResolvedTurnEnvironments,
|
||||
/// The session's absolute working directory. All relative paths provided
|
||||
/// by the model as well as sandbox policies are resolved against this path
|
||||
/// instead of `std::env::current_dir()`.
|
||||
@@ -106,10 +107,6 @@ impl TurnContext {
|
||||
self.permission_profile.network_sandbox_policy()
|
||||
}
|
||||
|
||||
pub(crate) fn primary_environment(&self) -> Option<&TurnEnvironment> {
|
||||
self.environments.first()
|
||||
}
|
||||
|
||||
pub(crate) fn sandbox_policy(&self) -> SandboxPolicy {
|
||||
let file_system_sandbox_policy = self.file_system_sandbox_policy();
|
||||
let network_sandbox_policy = self.network_sandbox_policy();
|
||||
@@ -198,7 +195,7 @@ impl TurnContext {
|
||||
.with_unified_exec_shell_mode(self.tools_config.unified_exec_shell_mode.clone())
|
||||
.with_web_search_config(self.tools_config.web_search_config.clone())
|
||||
.with_allow_login_shell(self.tools_config.allow_login_shell)
|
||||
.with_has_environment(self.tools_config.has_environment)
|
||||
.with_environment_mode(self.tools_config.environment_mode)
|
||||
.with_spawn_agent_usage_hint(config.multi_agent_v2.usage_hint_enabled)
|
||||
.with_spawn_agent_usage_hint_text(config.multi_agent_v2.usage_hint_text.clone())
|
||||
.with_hide_spawn_agent_metadata(config.multi_agent_v2.hide_spawn_agent_metadata)
|
||||
@@ -435,7 +432,7 @@ impl Session {
|
||||
model_info: ModelInfo,
|
||||
models_manager: &SharedModelsManager,
|
||||
network: Option<NetworkProxy>,
|
||||
environments: Vec<TurnEnvironment>,
|
||||
environments: ResolvedTurnEnvironments,
|
||||
cwd: AbsolutePathBuf,
|
||||
sub_id: String,
|
||||
skills_outcome: Arc<SkillLoadOutcome>,
|
||||
@@ -476,7 +473,9 @@ impl Session {
|
||||
)
|
||||
.with_web_search_config(per_turn_config.web_search_config.clone())
|
||||
.with_allow_login_shell(per_turn_config.permissions.allow_login_shell)
|
||||
.with_has_environment(!environments.is_empty())
|
||||
.with_environment_mode(ToolEnvironmentMode::from_count(
|
||||
environments.turn_environments.len(),
|
||||
))
|
||||
.with_spawn_agent_usage_hint(per_turn_config.multi_agent_v2.usage_hint_enabled)
|
||||
.with_spawn_agent_usage_hint_text(per_turn_config.multi_agent_v2.usage_hint_text.clone())
|
||||
.with_hide_spawn_agent_metadata(per_turn_config.multi_agent_v2.hide_spawn_agent_metadata)
|
||||
@@ -647,12 +646,11 @@ impl Session {
|
||||
fn resolve_turn_environments(
|
||||
&self,
|
||||
environments: &[TurnEnvironmentSelection],
|
||||
) -> CodexResult<Vec<TurnEnvironment>> {
|
||||
) -> CodexResult<ResolvedTurnEnvironments> {
|
||||
crate::environment_selection::resolve_environment_selections(
|
||||
self.services.environment_manager.as_ref(),
|
||||
environments,
|
||||
)
|
||||
.map(|resolved| resolved.turn_environments)
|
||||
}
|
||||
|
||||
async fn new_turn_from_configuration(
|
||||
@@ -660,9 +658,9 @@ impl Session {
|
||||
sub_id: String,
|
||||
session_configuration: SessionConfiguration,
|
||||
final_output_json_schema: Option<Option<Value>>,
|
||||
turn_environments: Vec<TurnEnvironment>,
|
||||
turn_environments: ResolvedTurnEnvironments,
|
||||
) -> Arc<TurnContext> {
|
||||
let primary_turn_environment = turn_environments.first();
|
||||
let primary_turn_environment = turn_environments.primary();
|
||||
let cwd = primary_turn_environment
|
||||
.map(|turn_environment| turn_environment.cwd.clone())
|
||||
.unwrap_or_else(|| session_configuration.cwd.clone());
|
||||
@@ -769,7 +767,7 @@ impl Session {
|
||||
Ok(turn_environments) => turn_environments,
|
||||
Err(err) => {
|
||||
warn!("failed to resolve stored session environments: {err}");
|
||||
Vec::new()
|
||||
ResolvedTurnEnvironments::default()
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use crate::StateDbHandle;
|
||||
use crate::rollout::list::find_thread_path_by_id_str;
|
||||
use crate::shell::Shell;
|
||||
use crate::shell::ShellType;
|
||||
@@ -41,6 +42,7 @@ impl ShellSnapshot {
|
||||
session_cwd: AbsolutePathBuf,
|
||||
shell: &mut Shell,
|
||||
session_telemetry: SessionTelemetry,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) -> watch::Sender<Option<Arc<ShellSnapshot>>> {
|
||||
let (shell_snapshot_tx, shell_snapshot_rx) = watch::channel(None);
|
||||
shell.shell_snapshot = shell_snapshot_rx;
|
||||
@@ -52,6 +54,7 @@ impl ShellSnapshot {
|
||||
shell.clone(),
|
||||
shell_snapshot_tx.clone(),
|
||||
session_telemetry,
|
||||
state_db,
|
||||
);
|
||||
|
||||
shell_snapshot_tx
|
||||
@@ -64,6 +67,7 @@ impl ShellSnapshot {
|
||||
shell: Shell,
|
||||
shell_snapshot_tx: watch::Sender<Option<Arc<ShellSnapshot>>>,
|
||||
session_telemetry: SessionTelemetry,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) {
|
||||
Self::spawn_snapshot_task(
|
||||
codex_home,
|
||||
@@ -72,6 +76,7 @@ impl ShellSnapshot {
|
||||
shell,
|
||||
shell_snapshot_tx,
|
||||
session_telemetry,
|
||||
state_db,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,15 +87,21 @@ impl ShellSnapshot {
|
||||
snapshot_shell: Shell,
|
||||
shell_snapshot_tx: watch::Sender<Option<Arc<ShellSnapshot>>>,
|
||||
session_telemetry: SessionTelemetry,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) {
|
||||
let snapshot_span = info_span!("shell_snapshot", thread_id = %session_id);
|
||||
tokio::spawn(
|
||||
async move {
|
||||
let timer = session_telemetry.start_timer("codex.shell_snapshot.duration_ms", &[]);
|
||||
let snapshot =
|
||||
ShellSnapshot::try_new(&codex_home, session_id, &session_cwd, &snapshot_shell)
|
||||
.await
|
||||
.map(Arc::new);
|
||||
let snapshot = ShellSnapshot::try_new(
|
||||
&codex_home,
|
||||
session_id,
|
||||
&session_cwd,
|
||||
&snapshot_shell,
|
||||
state_db,
|
||||
)
|
||||
.await
|
||||
.map(Arc::new);
|
||||
let success = snapshot.is_ok();
|
||||
let success_tag = if success { "true" } else { "false" };
|
||||
let _ = timer.map(|timer| timer.record(&[("success", success_tag)]));
|
||||
@@ -110,6 +121,7 @@ impl ShellSnapshot {
|
||||
session_id: ThreadId,
|
||||
session_cwd: &AbsolutePathBuf,
|
||||
shell: &Shell,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) -> std::result::Result<Self, &'static str> {
|
||||
// File to store the snapshot
|
||||
let extension = match shell.shell_type {
|
||||
@@ -131,7 +143,9 @@ impl ShellSnapshot {
|
||||
let codex_home = codex_home.clone();
|
||||
let cleanup_session_id = session_id;
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = cleanup_stale_snapshots(&codex_home, cleanup_session_id).await {
|
||||
if let Err(err) =
|
||||
cleanup_stale_snapshots(&codex_home, cleanup_session_id, state_db).await
|
||||
{
|
||||
tracing::warn!("Failed to clean up shell snapshots: {err:?}");
|
||||
}
|
||||
});
|
||||
@@ -486,6 +500,7 @@ $envVars | ForEach-Object {
|
||||
pub async fn cleanup_stale_snapshots(
|
||||
codex_home: &AbsolutePathBuf,
|
||||
active_session_id: ThreadId,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) -> Result<()> {
|
||||
let snapshot_dir = codex_home.join(SNAPSHOT_DIR);
|
||||
|
||||
@@ -515,7 +530,8 @@ pub async fn cleanup_stale_snapshots(
|
||||
continue;
|
||||
}
|
||||
|
||||
let rollout_path = find_thread_path_by_id_str(codex_home, session_id).await?;
|
||||
let rollout_path =
|
||||
find_thread_path_by_id_str(codex_home, session_id, state_db.as_deref()).await?;
|
||||
let Some(rollout_path) = rollout_path else {
|
||||
remove_snapshot_file(&path).await;
|
||||
continue;
|
||||
|
||||
@@ -202,6 +202,7 @@ async fn try_new_creates_and_deletes_snapshot_file() -> Result<()> {
|
||||
ThreadId::new(),
|
||||
&dir.path().abs(),
|
||||
&shell,
|
||||
/*state_db*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("snapshot should be created");
|
||||
@@ -227,14 +228,24 @@ async fn try_new_uses_distinct_generation_paths() -> Result<()> {
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
};
|
||||
|
||||
let initial_snapshot =
|
||||
ShellSnapshot::try_new(&dir.path().abs(), session_id, &dir.path().abs(), &shell)
|
||||
.await
|
||||
.expect("initial snapshot should be created");
|
||||
let refreshed_snapshot =
|
||||
ShellSnapshot::try_new(&dir.path().abs(), session_id, &dir.path().abs(), &shell)
|
||||
.await
|
||||
.expect("refreshed snapshot should be created");
|
||||
let initial_snapshot = ShellSnapshot::try_new(
|
||||
&dir.path().abs(),
|
||||
session_id,
|
||||
&dir.path().abs(),
|
||||
&shell,
|
||||
/*state_db*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("initial snapshot should be created");
|
||||
let refreshed_snapshot = ShellSnapshot::try_new(
|
||||
&dir.path().abs(),
|
||||
session_id,
|
||||
&dir.path().abs(),
|
||||
&shell,
|
||||
/*state_db*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("refreshed snapshot should be created");
|
||||
let initial_path = initial_snapshot.path.clone();
|
||||
let refreshed_path = refreshed_snapshot.path.clone();
|
||||
|
||||
@@ -428,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()).await?;
|
||||
cleanup_stale_snapshots(&codex_home, ThreadId::new(), /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(live_snapshot.exists(), true);
|
||||
assert_eq!(orphan_snapshot.exists(), false);
|
||||
@@ -451,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()).await?;
|
||||
cleanup_stale_snapshots(&codex_home, ThreadId::new(), /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(stale_snapshot.exists(), false);
|
||||
Ok(())
|
||||
@@ -472,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).await?;
|
||||
cleanup_stale_snapshots(&codex_home, active_session, /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(active_snapshot.exists(), true);
|
||||
Ok(())
|
||||
|
||||
@@ -3,6 +3,6 @@ pub use codex_rollout::state_db::StateDbHandle;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
pub async fn get_state_db(config: &Config) -> Option<StateDbHandle> {
|
||||
rollout_state_db::get_state_db(config).await
|
||||
pub async fn init_state_db(config: &Config) -> Option<StateDbHandle> {
|
||||
rollout_state_db::init(config).await
|
||||
}
|
||||
|
||||
@@ -138,8 +138,11 @@ pub(crate) async fn record_completed_response_item(
|
||||
.await;
|
||||
}
|
||||
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(turn_context, item).await;
|
||||
let has_memory_citation = record_stage1_output_usage_and_detect_memory_citation(
|
||||
sess.services.state_db.as_ref(),
|
||||
item,
|
||||
)
|
||||
.await;
|
||||
if has_memory_citation {
|
||||
sess.record_memory_citation_for_turn(&turn_context.sub_id)
|
||||
.await;
|
||||
@@ -174,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(
|
||||
turn_context: &TurnContext,
|
||||
state_db_ctx: Option<&state_db::StateDbHandle>,
|
||||
item: &ResponseItem,
|
||||
) -> bool {
|
||||
let Some(raw_text) = raw_assistant_output_text_from_item(item) else {
|
||||
@@ -190,7 +193,7 @@ async fn record_stage1_output_usage_and_detect_memory_citation(
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(db) = state_db::get_state_db(turn_context.config.as_ref()).await {
|
||||
if let Some(db) = state_db_ctx {
|
||||
let _ = db.record_stage1_output_usage(&thread_ids).await;
|
||||
}
|
||||
true
|
||||
|
||||
@@ -73,6 +73,22 @@ pub fn thread_manager_with_models_provider_and_home(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn thread_manager_with_models_provider_home_and_state(
|
||||
auth: CodexAuth,
|
||||
provider: ModelProviderInfo,
|
||||
codex_home: PathBuf,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
state_db: Option<crate::StateDbHandle>,
|
||||
) -> ThreadManager {
|
||||
ThreadManager::with_models_provider_home_and_state_for_tests(
|
||||
auth,
|
||||
provider,
|
||||
codex_home,
|
||||
environment_manager,
|
||||
state_db,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn start_thread_with_user_shell_override(
|
||||
thread_manager: &ThreadManager,
|
||||
config: Config,
|
||||
|
||||
@@ -50,6 +50,7 @@ use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_rollout::state_db::StateDbHandle;
|
||||
use codex_state::DirectionalThreadSpawnEdgeStatus;
|
||||
use codex_thread_store::InMemoryThreadStore;
|
||||
use codex_thread_store::LocalThreadStore;
|
||||
@@ -248,6 +249,7 @@ pub(crate) struct ThreadManagerState {
|
||||
thread_store: Arc<dyn ThreadStore>,
|
||||
session_source: SessionSource,
|
||||
analytics_events_client: Option<AnalyticsEventsClient>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
// Captures submitted ops for testing purpose when test mode is enabled.
|
||||
ops_log: Option<SharedCapturedOps>,
|
||||
}
|
||||
@@ -263,10 +265,14 @@ pub fn build_models_manager(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn thread_store_from_config(config: &Config) -> Arc<dyn ThreadStore> {
|
||||
pub fn thread_store_from_config(
|
||||
config: &Config,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) -> Arc<dyn ThreadStore> {
|
||||
match &config.experimental_thread_store {
|
||||
ThreadStoreConfig::Local => Arc::new(LocalThreadStore::new(
|
||||
LocalThreadStoreConfig::from_config(config),
|
||||
state_db,
|
||||
)),
|
||||
ThreadStoreConfig::Remote { endpoint } => Arc::new(RemoteThreadStore::new(endpoint)),
|
||||
ThreadStoreConfig::InMemory { id } => InMemoryThreadStore::for_id(id),
|
||||
@@ -281,6 +287,7 @@ impl ThreadManager {
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
analytics_events_client: Option<AnalyticsEventsClient>,
|
||||
thread_store: Arc<dyn ThreadStore>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) -> Self {
|
||||
let codex_home = config.codex_home.clone();
|
||||
let restriction_product = session_source.restriction_product();
|
||||
@@ -310,6 +317,7 @@ impl ThreadManager {
|
||||
auth_manager,
|
||||
session_source,
|
||||
analytics_events_client,
|
||||
state_db,
|
||||
ops_log: should_use_test_thread_manager_behavior()
|
||||
.then(|| Arc::new(std::sync::Mutex::new(Vec::new()))),
|
||||
}),
|
||||
@@ -347,6 +355,22 @@ impl ThreadManager {
|
||||
provider: ModelProviderInfo,
|
||||
codex_home: PathBuf,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
) -> Self {
|
||||
Self::with_models_provider_home_and_state_for_tests(
|
||||
auth,
|
||||
provider,
|
||||
codex_home,
|
||||
environment_manager,
|
||||
/*state_db*/ None,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn with_models_provider_home_and_state_for_tests(
|
||||
auth: CodexAuth,
|
||||
provider: ModelProviderInfo,
|
||||
codex_home: PathBuf,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) -> Self {
|
||||
set_thread_manager_test_mode_for_tests(/*enabled*/ true);
|
||||
let auth_manager = AuthManager::from_auth_for_testing(auth);
|
||||
@@ -369,12 +393,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 thread_store: Arc<dyn ThreadStore> =
|
||||
Arc::new(LocalThreadStore::new(LocalThreadStoreConfig {
|
||||
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(),
|
||||
));
|
||||
Self {
|
||||
state: Arc::new(ThreadManagerState {
|
||||
threads: Arc::new(RwLock::new(HashMap::new())),
|
||||
@@ -390,6 +416,7 @@ impl ThreadManager {
|
||||
auth_manager,
|
||||
session_source: SessionSource::Exec,
|
||||
analytics_events_client: None,
|
||||
state_db,
|
||||
ops_log: should_use_test_thread_manager_behavior()
|
||||
.then(|| Arc::new(std::sync::Mutex::new(Vec::new()))),
|
||||
}),
|
||||
@@ -837,6 +864,10 @@ impl ThreadManager {
|
||||
}
|
||||
|
||||
impl ThreadManagerState {
|
||||
pub(crate) fn state_db(&self) -> Option<StateDbHandle> {
|
||||
self.state_db.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn list_thread_ids(&self) -> Vec<ThreadId> {
|
||||
self.threads
|
||||
.read()
|
||||
@@ -1100,7 +1131,7 @@ impl ThreadManagerState {
|
||||
}
|
||||
let environment_selections =
|
||||
resolve_environment_selections(self.environment_manager.as_ref(), &environments)?;
|
||||
let watch_registration = match environment_selections.primary_turn_environment() {
|
||||
let watch_registration = match environment_selections.primary() {
|
||||
Some(turn_environment) if !turn_environment.environment.is_remote() => {
|
||||
self.skills_watcher
|
||||
.register_config(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use crate::config::test_config;
|
||||
use crate::init_state_db;
|
||||
use crate::rollout::RolloutRecorder;
|
||||
use crate::session::session::SessionSettingsUpdate;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
@@ -389,7 +390,8 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
||||
SessionSource::Exec,
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config),
|
||||
thread_store_from_config(&config, /*state_db*/ None),
|
||||
/*state_db*/ None,
|
||||
);
|
||||
let selected_cwd =
|
||||
AbsolutePathBuf::try_from(config.cwd.as_path().join("selected")).expect("absolute path");
|
||||
@@ -444,9 +446,15 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
||||
.new_turn_with_sub_id("resume-turn".to_string(), SessionSettingsUpdate::default())
|
||||
.await
|
||||
.expect("build resumed turn context");
|
||||
assert_eq!(resumed_turn.environments.len(), 1);
|
||||
assert_eq!(resumed_turn.environments[0].cwd, default_cwd);
|
||||
assert_ne!(resumed_turn.environments[0].cwd, selected_cwd);
|
||||
assert_eq!(resumed_turn.environments.turn_environments.len(), 1);
|
||||
assert_eq!(
|
||||
resumed_turn.environments.turn_environments[0].cwd,
|
||||
default_cwd
|
||||
);
|
||||
assert_ne!(
|
||||
resumed_turn.environments.turn_environments[0].cwd,
|
||||
selected_cwd
|
||||
);
|
||||
|
||||
let forked = manager
|
||||
.fork_thread(
|
||||
@@ -465,9 +473,15 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
||||
.new_turn_with_sub_id("fork-turn".to_string(), SessionSettingsUpdate::default())
|
||||
.await
|
||||
.expect("build forked turn context");
|
||||
assert_eq!(forked_turn.environments.len(), 1);
|
||||
assert_eq!(forked_turn.environments[0].cwd, default_cwd);
|
||||
assert_ne!(forked_turn.environments[0].cwd, selected_cwd);
|
||||
assert_eq!(forked_turn.environments.turn_environments.len(), 1);
|
||||
assert_eq!(
|
||||
forked_turn.environments.turn_environments[0].cwd,
|
||||
default_cwd
|
||||
);
|
||||
assert_ne!(
|
||||
forked_turn.environments.turn_environments[0].cwd,
|
||||
selected_cwd
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -486,7 +500,8 @@ async fn resume_active_thread_from_rollout_returns_running_thread() {
|
||||
SessionSource::Exec,
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config),
|
||||
thread_store_from_config(&config, /*state_db*/ None),
|
||||
/*state_db*/ None,
|
||||
);
|
||||
|
||||
let source = manager
|
||||
@@ -539,7 +554,8 @@ async fn resume_stopped_thread_from_rollout_spawns_new_thread() {
|
||||
SessionSource::Exec,
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config),
|
||||
thread_store_from_config(&config, /*state_db*/ None),
|
||||
/*state_db*/ None,
|
||||
);
|
||||
|
||||
let source = manager
|
||||
@@ -602,7 +618,8 @@ async fn new_uses_active_provider_for_model_refresh() {
|
||||
SessionSource::Exec,
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config),
|
||||
thread_store_from_config(&config, /*state_db*/ None),
|
||||
/*state_db*/ None,
|
||||
);
|
||||
|
||||
let _ = manager.list_models(RefreshStrategy::Online).await;
|
||||
@@ -807,13 +824,15 @@ 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 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),
|
||||
thread_store_from_config(&config, state_db.clone()),
|
||||
state_db.clone(),
|
||||
);
|
||||
|
||||
let source = manager
|
||||
@@ -909,13 +928,15 @@ 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 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),
|
||||
thread_store_from_config(&config, state_db.clone()),
|
||||
state_db.clone(),
|
||||
);
|
||||
|
||||
let source = manager
|
||||
@@ -1000,13 +1021,15 @@ 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 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),
|
||||
thread_store_from_config(&config, state_db.clone()),
|
||||
state_db.clone(),
|
||||
);
|
||||
|
||||
let source = manager
|
||||
@@ -1136,13 +1159,15 @@ 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 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),
|
||||
thread_store_from_config(&config, state_db.clone()),
|
||||
state_db.clone(),
|
||||
);
|
||||
|
||||
let source = manager
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::config::Config;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
@@ -541,12 +540,7 @@ async fn run_agent_job_loop(
|
||||
"agent_job:{job_id}"
|
||||
)))),
|
||||
SpawnAgentOptions {
|
||||
environments: Some(
|
||||
turn.environments
|
||||
.iter()
|
||||
.map(TurnEnvironment::selection)
|
||||
.collect(),
|
||||
),
|
||||
environments: Some(turn.environments.to_selections()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -256,14 +256,6 @@ fn apply_patch_payload_command(payload: &ToolPayload) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_patch_mode(turn: &TurnContext) -> ParsePatchMode {
|
||||
if turn.features.enabled(Feature::ApplyPatchStreamingParser) {
|
||||
ParsePatchMode::Streaming
|
||||
} else {
|
||||
ParsePatchMode::Legacy
|
||||
}
|
||||
}
|
||||
|
||||
async fn effective_patch_permissions(
|
||||
session: &Session,
|
||||
turn: &TurnContext,
|
||||
@@ -372,7 +364,7 @@ impl ToolHandler for ApplyPatchHandler {
|
||||
// Avoid building temporary ExecParams/command vectors; derive directly from inputs.
|
||||
let cwd = turn.cwd.clone();
|
||||
let command = vec!["apply_patch".to_string(), patch_input.clone()];
|
||||
let Some(turn_environment) = turn.primary_environment() else {
|
||||
let Some(turn_environment) = turn.environments.primary() else {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"apply_patch is unavailable in this session".to_string(),
|
||||
));
|
||||
@@ -382,8 +374,12 @@ impl ToolHandler for ApplyPatchHandler {
|
||||
.environment
|
||||
.is_remote()
|
||||
.then(|| turn.file_system_sandbox_context(/*additional_permissions*/ None));
|
||||
let parse_mode = apply_patch_mode(turn.as_ref());
|
||||
match codex_apply_patch::maybe_parse_apply_patch_verified_with_mode(
|
||||
let parse_mode = if turn.features.enabled(Feature::ApplyPatchStreamingParser) {
|
||||
ParsePatchMode::Streaming
|
||||
} else {
|
||||
ParsePatchMode::Legacy
|
||||
};
|
||||
match codex_apply_patch::maybe_parse_apply_patch_verified(
|
||||
&command,
|
||||
parse_mode,
|
||||
&cwd,
|
||||
@@ -416,6 +412,7 @@ impl ToolHandler for ApplyPatchHandler {
|
||||
|
||||
let req = ApplyPatchRequest {
|
||||
action: apply.action,
|
||||
parse_mode,
|
||||
file_paths,
|
||||
changes,
|
||||
exec_approval_requirement: apply.exec_approval_requirement,
|
||||
@@ -486,11 +483,16 @@ pub(crate) async fn intercept_apply_patch(
|
||||
tool_name: &str,
|
||||
) -> Result<Option<FunctionToolOutput>, FunctionCallError> {
|
||||
let sandbox = turn
|
||||
.primary_environment()
|
||||
.environments
|
||||
.primary()
|
||||
.filter(|env| env.environment.is_remote())
|
||||
.map(|_| turn.file_system_sandbox_context(/*additional_permissions*/ None));
|
||||
let parse_mode = apply_patch_mode(turn.as_ref());
|
||||
match codex_apply_patch::maybe_parse_apply_patch_verified_with_mode(
|
||||
let parse_mode = if turn.features.enabled(Feature::ApplyPatchStreamingParser) {
|
||||
ParsePatchMode::Streaming
|
||||
} else {
|
||||
ParsePatchMode::Legacy
|
||||
};
|
||||
match codex_apply_patch::maybe_parse_apply_patch_verified(
|
||||
command,
|
||||
parse_mode,
|
||||
cwd,
|
||||
@@ -530,6 +532,7 @@ pub(crate) async fn intercept_apply_patch(
|
||||
|
||||
let req = ApplyPatchRequest {
|
||||
action: apply.action,
|
||||
parse_mode,
|
||||
file_paths: approval_keys,
|
||||
changes,
|
||||
exec_approval_requirement: apply.exec_approval_requirement,
|
||||
|
||||
@@ -99,18 +99,6 @@ async fn post_tool_use_payload_uses_patch_input_and_tool_output() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_patch_mode_follows_streaming_parser_feature() {
|
||||
let (_, mut turn) = make_session_and_context().await;
|
||||
assert_eq!(apply_patch_mode(&turn), ParsePatchMode::Legacy);
|
||||
|
||||
turn.features
|
||||
.enable(Feature::ApplyPatchStreamingParser)
|
||||
.expect("enable feature");
|
||||
|
||||
assert_eq!(apply_patch_mode(&turn), ParsePatchMode::Streaming);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_consumer_does_not_stream_json_tool_call_arguments() {
|
||||
let mut consumer = ApplyPatchArgumentDiffConsumer::default();
|
||||
@@ -234,6 +222,7 @@ async fn approval_keys_include_move_destination() {
|
||||
let argv = vec!["apply_patch".to_string(), patch.to_string()];
|
||||
let action = match codex_apply_patch::maybe_parse_apply_patch_verified(
|
||||
&argv,
|
||||
codex_apply_patch::ParsePatchMode::Legacy,
|
||||
&cwd,
|
||||
LOCAL_FS.as_ref(),
|
||||
/*sandbox*/ None,
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::agent::exceeds_thread_spawn_depth_limit;
|
||||
use crate::agent::next_thread_spawn_depth;
|
||||
use crate::agent::role::DEFAULT_ROLE_NAME;
|
||||
use crate::agent::role::apply_role_to_config;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
|
||||
@@ -83,29 +82,22 @@ impl ToolHandler for Handler {
|
||||
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
|
||||
apply_spawn_agent_overrides(&mut config, child_depth);
|
||||
|
||||
let result = Box::pin(
|
||||
session.services.agent_control.spawn_agent_with_metadata(
|
||||
config,
|
||||
input_items,
|
||||
Some(thread_spawn_source(
|
||||
session.conversation_id,
|
||||
&turn.session_source,
|
||||
child_depth,
|
||||
role_name,
|
||||
/*task_name*/ None,
|
||||
)?),
|
||||
SpawnAgentOptions {
|
||||
fork_parent_spawn_call_id: args.fork_context.then(|| call_id.clone()),
|
||||
fork_mode: args.fork_context.then_some(SpawnAgentForkMode::FullHistory),
|
||||
environments: Some(
|
||||
turn.environments
|
||||
.iter()
|
||||
.map(TurnEnvironment::selection)
|
||||
.collect(),
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
let result = Box::pin(session.services.agent_control.spawn_agent_with_metadata(
|
||||
config,
|
||||
input_items,
|
||||
Some(thread_spawn_source(
|
||||
session.conversation_id,
|
||||
&turn.session_source,
|
||||
child_depth,
|
||||
role_name,
|
||||
/*task_name*/ None,
|
||||
)?),
|
||||
SpawnAgentOptions {
|
||||
fork_parent_spawn_call_id: args.fork_context.then(|| call_id.clone()),
|
||||
fork_mode: args.fork_context.then_some(SpawnAgentForkMode::FullHistory),
|
||||
environments: Some(turn.environments.to_selections()),
|
||||
},
|
||||
))
|
||||
.await
|
||||
.map_err(collab_spawn_error);
|
||||
let (new_thread_id, new_agent_metadata, status) = match &result {
|
||||
|
||||
@@ -3,8 +3,10 @@ use crate::ThreadManager;
|
||||
use crate::config::AgentRoleConfig;
|
||||
use crate::config::DEFAULT_AGENT_MAX_DEPTH;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::init_state_db;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use crate::session_prefix::format_subagent_notification_message;
|
||||
use crate::thread_manager::thread_store_from_config;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::handlers::multi_agents_v2::CloseAgentHandler as CloseAgentHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::FollowupTaskHandler as FollowupTaskHandlerV2;
|
||||
@@ -3149,13 +3151,22 @@ async fn close_agent_submits_shutdown_and_returns_previous_status() {
|
||||
#[tokio::test]
|
||||
async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtrees_closed() {
|
||||
let (_session, turn) = make_session_and_context().await;
|
||||
let manager = thread_manager();
|
||||
let mut config = turn.config.as_ref().clone();
|
||||
config.agent_max_depth = 3;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::Sqlite)
|
||||
.expect("test config should allow sqlite");
|
||||
let state_db = init_state_db(&config).await;
|
||||
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(),
|
||||
);
|
||||
|
||||
let parent = manager
|
||||
.start_thread(config.clone())
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::agent::control::render_input_preview;
|
||||
use crate::agent::next_thread_spawn_depth;
|
||||
use crate::agent::role::DEFAULT_ROLE_NAME;
|
||||
use crate::agent::role::apply_role_to_config;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_protocol::protocol::Op;
|
||||
@@ -118,12 +117,7 @@ impl ToolHandler for Handler {
|
||||
SpawnAgentOptions {
|
||||
fork_parent_spawn_call_id: fork_mode.as_ref().map(|_| call_id.clone()),
|
||||
fork_mode,
|
||||
environments: Some(
|
||||
turn.environments
|
||||
.iter()
|
||||
.map(TurnEnvironment::selection)
|
||||
.collect(),
|
||||
),
|
||||
environments: Some(turn.environments.to_selections()),
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -412,7 +412,7 @@ impl ShellHandler {
|
||||
} = args;
|
||||
|
||||
let mut exec_params = exec_params;
|
||||
let Some(turn_environment) = turn.primary_environment() else {
|
||||
let Some(turn_environment) = turn.environments.primary() else {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"shell is unavailable in this session".to_string(),
|
||||
));
|
||||
|
||||
@@ -196,7 +196,7 @@ impl ToolHandler for UnifiedExecHandler {
|
||||
}
|
||||
};
|
||||
|
||||
let Some(turn_environment) = turn.primary_environment() else {
|
||||
let Some(turn_environment) = turn.environments.primary() else {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"unified exec is unavailable in this session".to_string(),
|
||||
));
|
||||
|
||||
@@ -88,7 +88,7 @@ impl ToolHandler for ViewImageHandler {
|
||||
};
|
||||
|
||||
let abs_path = turn.resolve_path(Some(args.path));
|
||||
let Some(environment) = turn.primary_environment() else {
|
||||
let Some(environment) = turn.environments.primary() else {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"view_image is unavailable in this session".to_string(),
|
||||
));
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::tools::sandboxing::ToolError;
|
||||
use crate::tools::sandboxing::ToolRuntime;
|
||||
use crate::tools::sandboxing::with_cached_approval;
|
||||
use codex_apply_patch::ApplyPatchAction;
|
||||
use codex_apply_patch::ParsePatchMode;
|
||||
use codex_exec_server::FileSystemSandboxContext;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
@@ -38,6 +39,7 @@ use std::time::Instant;
|
||||
#[derive(Debug)]
|
||||
pub struct ApplyPatchRequest {
|
||||
pub action: ApplyPatchAction,
|
||||
pub parse_mode: ParsePatchMode,
|
||||
pub file_paths: Vec<AbsolutePathBuf>,
|
||||
pub changes: std::collections::HashMap<PathBuf, FileChange>,
|
||||
pub exec_approval_requirement: ExecApprovalRequirement,
|
||||
@@ -191,7 +193,7 @@ impl ToolRuntime<ApplyPatchRequest, ExecToolCallOutput> for ApplyPatchRuntime {
|
||||
attempt: &SandboxAttempt<'_>,
|
||||
ctx: &ToolCtx,
|
||||
) -> Result<ExecToolCallOutput, ToolError> {
|
||||
let turn_environment = ctx.turn.primary_environment().ok_or_else(|| {
|
||||
let turn_environment = ctx.turn.environments.primary().ok_or_else(|| {
|
||||
ToolError::Rejected("apply_patch is unavailable in this session".to_string())
|
||||
})?;
|
||||
let started_at = Instant::now();
|
||||
@@ -199,8 +201,9 @@ impl ToolRuntime<ApplyPatchRequest, ExecToolCallOutput> for ApplyPatchRuntime {
|
||||
let sandbox = Self::file_system_sandbox_context_for_attempt(req, attempt);
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let result = codex_apply_patch::apply_hunks(
|
||||
req.action.hunks(),
|
||||
let result = codex_apply_patch::apply_patch(
|
||||
&req.action.patch,
|
||||
req.parse_mode,
|
||||
&req.action.cwd,
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
|
||||
@@ -50,6 +50,7 @@ fn guardian_review_request_includes_patch_context() {
|
||||
let expected_patch = action.patch.clone();
|
||||
let request = ApplyPatchRequest {
|
||||
action,
|
||||
parse_mode: ParsePatchMode::Legacy,
|
||||
file_paths: vec![path.clone()],
|
||||
changes: HashMap::from([(
|
||||
path.to_path_buf(),
|
||||
@@ -88,6 +89,7 @@ fn permission_request_payload_uses_apply_patch_hook_name_and_aliases() {
|
||||
let expected_patch = action.patch.clone();
|
||||
let req = ApplyPatchRequest {
|
||||
action,
|
||||
parse_mode: ParsePatchMode::Legacy,
|
||||
file_paths: vec![path],
|
||||
changes: HashMap::new(),
|
||||
exec_approval_requirement: ExecApprovalRequirement::NeedsApproval {
|
||||
@@ -127,6 +129,7 @@ fn file_system_sandbox_context_uses_active_attempt() {
|
||||
};
|
||||
let req = ApplyPatchRequest {
|
||||
action: ApplyPatchAction::new_add_for_test(&path, "hello".to_string()),
|
||||
parse_mode: ParsePatchMode::Legacy,
|
||||
file_paths: vec![path.clone()],
|
||||
changes: HashMap::new(),
|
||||
exec_approval_requirement: ExecApprovalRequirement::Skip {
|
||||
@@ -184,6 +187,7 @@ fn no_sandbox_attempt_has_no_file_system_context() {
|
||||
.abs();
|
||||
let req = ApplyPatchRequest {
|
||||
action: ApplyPatchAction::new_add_for_test(&path, "hello".to_string()),
|
||||
parse_mode: ParsePatchMode::Legacy,
|
||||
file_paths: vec![path.clone()],
|
||||
changes: HashMap::new(),
|
||||
exec_approval_requirement: ExecApprovalRequirement::Skip {
|
||||
|
||||
@@ -254,7 +254,8 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
}
|
||||
let environment_is_remote = ctx
|
||||
.turn
|
||||
.primary_environment()
|
||||
.environments
|
||||
.primary()
|
||||
.is_some_and(|turn_environment| turn_environment.environment.is_remote());
|
||||
let command = if environment_is_remote {
|
||||
base_command.to_vec()
|
||||
@@ -292,7 +293,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
.await?
|
||||
{
|
||||
Some(prepared) => {
|
||||
let Some(turn_environment) = ctx.turn.primary_environment() else {
|
||||
let Some(turn_environment) = ctx.turn.environments.primary() else {
|
||||
return Err(ToolError::Rejected(
|
||||
"exec_command is unavailable in this session".to_string(),
|
||||
));
|
||||
@@ -337,7 +338,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
.env_for(command, options, managed_network)
|
||||
.map_err(|err| ToolError::Codex(err.into()))?;
|
||||
exec_env.exec_server_env_config = req.exec_server_env_config.clone();
|
||||
let Some(turn_environment) = ctx.turn.primary_environment() else {
|
||||
let Some(turn_environment) = ctx.turn.environments.primary() else {
|
||||
return Err(ToolError::Rejected(
|
||||
"exec_command is unavailable in this session".to_string(),
|
||||
));
|
||||
|
||||
@@ -96,7 +96,8 @@ async fn exec_command_with_tty(
|
||||
&request,
|
||||
tty,
|
||||
Box::new(NoopSpawnLifecycle),
|
||||
turn.primary_environment()
|
||||
turn.environments
|
||||
.primary()
|
||||
.expect("turn environment")
|
||||
.environment
|
||||
.as_ref(),
|
||||
@@ -594,7 +595,8 @@ async fn remote_exec_server_rejects_inherited_fd_launches() -> anyhow::Result<()
|
||||
|
||||
let remote_test_env = remote_test_env().await?;
|
||||
let (_, mut turn) = make_session_and_context().await;
|
||||
turn.environments[0].environment = Arc::new(remote_test_env.environment().clone());
|
||||
turn.environments.turn_environments[0].environment =
|
||||
Arc::new(remote_test_env.environment().clone());
|
||||
|
||||
let request = test_exec_request(
|
||||
&turn,
|
||||
@@ -612,7 +614,8 @@ async fn remote_exec_server_rejects_inherited_fd_launches() -> anyhow::Result<()
|
||||
Box::new(TestSpawnLifecycle {
|
||||
inherited_fds: vec![42],
|
||||
}),
|
||||
turn.primary_environment()
|
||||
turn.environments
|
||||
.primary()
|
||||
.expect("turn environment")
|
||||
.environment
|
||||
.as_ref(),
|
||||
|
||||
@@ -423,6 +423,7 @@ 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_manager = if config.model_catalog.is_some() {
|
||||
ThreadManager::new(
|
||||
&config,
|
||||
@@ -430,14 +431,16 @@ impl TestCodexBuilder {
|
||||
SessionSource::Exec,
|
||||
Arc::clone(&environment_manager),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config),
|
||||
thread_store_from_config(&config, state_db.clone()),
|
||||
state_db.clone(),
|
||||
)
|
||||
} else {
|
||||
codex_core::test_support::thread_manager_with_models_provider_and_home(
|
||||
codex_core::test_support::thread_manager_with_models_provider_home_and_state(
|
||||
auth.clone(),
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
Arc::clone(&environment_manager),
|
||||
state_db.clone(),
|
||||
)
|
||||
};
|
||||
let thread_manager = Arc::new(thread_manager);
|
||||
|
||||
@@ -219,6 +219,51 @@ D delete.txt
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn apply_patch_cli_uses_streaming_parser_when_feature_enabled() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let harness = apply_patch_harness_with(|builder| {
|
||||
builder.with_model("gpt-5.4").with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ApplyPatchStreamingParser)
|
||||
.expect("enable apply_patch streaming parser");
|
||||
config.suppress_unstable_features_warning = true;
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
let patch = "*** Begin Patch\n*** Add File: streaming-parser.txt\n+enabled\n*** End Patch";
|
||||
let call_id = "apply-streaming-parser";
|
||||
mount_apply_patch(
|
||||
&harness,
|
||||
call_id,
|
||||
patch,
|
||||
"done",
|
||||
ApplyPatchModelOutput::Function,
|
||||
)
|
||||
.await;
|
||||
|
||||
harness
|
||||
.submit("please apply streaming parser patch")
|
||||
.await?;
|
||||
|
||||
let out = harness
|
||||
.apply_patch_output(call_id, ApplyPatchModelOutput::Function)
|
||||
.await;
|
||||
assert_regex_match(
|
||||
r"(?s)^Exit code: 0.*Success\. Updated the following files:\nA streaming-parser\.txt\n?$",
|
||||
&out,
|
||||
);
|
||||
assert_eq!(
|
||||
harness.read_file_text("streaming-parser.txt").await?,
|
||||
"enabled\n"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[test_case(ApplyPatchModelOutput::Freeform)]
|
||||
#[test_case(ApplyPatchModelOutput::Function)]
|
||||
|
||||
@@ -1112,7 +1112,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),
|
||||
thread_store_from_config(&config, /*state_db*/ None),
|
||||
/*state_db*/ None,
|
||||
);
|
||||
let NewThread { thread: codex, .. } = thread_manager
|
||||
.start_thread(config.clone())
|
||||
|
||||
@@ -141,7 +141,8 @@ async fn migration_marker_exists_no_sessions_no_change() -> io::Result<()> {
|
||||
let marker_path = temp.path().join(PERSONALITY_MIGRATION_FILENAME);
|
||||
tokio::fs::write(&marker_path, "v1\n").await?;
|
||||
|
||||
let status = maybe_migrate_personality(temp.path(), &ConfigToml::default()).await?;
|
||||
let status =
|
||||
maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(status, PersonalityMigrationStatus::SkippedMarker);
|
||||
assert_eq!(
|
||||
@@ -155,7 +156,8 @@ async fn migration_marker_exists_no_sessions_no_change() -> io::Result<()> {
|
||||
async fn no_marker_no_sessions_no_change() -> io::Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
|
||||
let status = maybe_migrate_personality(temp.path(), &ConfigToml::default()).await?;
|
||||
let status =
|
||||
maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(status, PersonalityMigrationStatus::SkippedNoSessions);
|
||||
assert_eq!(
|
||||
@@ -174,7 +176,8 @@ async fn no_marker_sessions_sets_personality() -> io::Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
write_session_with_user_event(temp.path()).await?;
|
||||
|
||||
let status = maybe_migrate_personality(temp.path(), &ConfigToml::default()).await?;
|
||||
let status =
|
||||
maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(status, PersonalityMigrationStatus::Applied);
|
||||
assert_eq!(
|
||||
@@ -194,7 +197,7 @@ 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).await?;
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(status, PersonalityMigrationStatus::Applied);
|
||||
let persisted = read_config_toml(temp.path()).await?;
|
||||
@@ -208,7 +211,8 @@ async fn no_marker_meta_only_rollout_is_treated_as_no_sessions() -> io::Result<(
|
||||
let temp = TempDir::new()?;
|
||||
write_session_with_meta_only(temp.path()).await?;
|
||||
|
||||
let status = maybe_migrate_personality(temp.path(), &ConfigToml::default()).await?;
|
||||
let status =
|
||||
maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(status, PersonalityMigrationStatus::SkippedNoSessions);
|
||||
assert_eq!(
|
||||
@@ -228,7 +232,7 @@ 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).await?;
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
@@ -258,7 +262,7 @@ personality = "friendly"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml).await?;
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
@@ -281,7 +285,7 @@ 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).await?;
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(status, PersonalityMigrationStatus::SkippedMarker);
|
||||
Ok(())
|
||||
@@ -292,7 +296,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)
|
||||
let err = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None)
|
||||
.await
|
||||
.expect_err("missing profile should fail");
|
||||
|
||||
@@ -309,8 +313,10 @@ async fn applied_migration_is_idempotent_on_second_run() -> io::Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
write_session_with_user_event(temp.path()).await?;
|
||||
|
||||
let first_status = maybe_migrate_personality(temp.path(), &ConfigToml::default()).await?;
|
||||
let second_status = maybe_migrate_personality(temp.path(), &ConfigToml::default()).await?;
|
||||
let first_status =
|
||||
maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?;
|
||||
let second_status =
|
||||
maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(first_status, PersonalityMigrationStatus::Applied);
|
||||
assert_eq!(second_status, PersonalityMigrationStatus::SkippedMarker);
|
||||
@@ -324,7 +330,8 @@ async fn no_marker_archived_sessions_sets_personality() -> io::Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
write_archived_session_with_user_event(temp.path()).await?;
|
||||
|
||||
let status = maybe_migrate_personality(temp.path(), &ConfigToml::default()).await?;
|
||||
let status =
|
||||
maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?;
|
||||
|
||||
assert_eq!(status, PersonalityMigrationStatus::Applied);
|
||||
assert_eq!(
|
||||
|
||||
@@ -29,6 +29,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,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ use codex_core::find_thread_path_by_id_str;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_rollout::StateDbHandle;
|
||||
use codex_state::StateRuntime;
|
||||
use codex_state::ThreadMetadataBuilder;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -27,7 +28,13 @@ fn write_minimal_rollout_with_id_in_subdir(codex_home: &Path, subdir: &str, id:
|
||||
std::fs::create_dir_all(&sessions).unwrap();
|
||||
|
||||
let file = sessions.join(format!("rollout-2024-01-01T00-00-00-{id}.jsonl"));
|
||||
let mut f = std::fs::File::create(&file).unwrap();
|
||||
write_minimal_rollout_with_id_at_path(&file, id);
|
||||
|
||||
file
|
||||
}
|
||||
|
||||
fn write_minimal_rollout_with_id_at_path(file: &Path, id: Uuid) {
|
||||
let mut f = std::fs::File::create(file).unwrap();
|
||||
// Minimal first line: session_meta with the id so content search can find it
|
||||
writeln!(
|
||||
f,
|
||||
@@ -46,8 +53,6 @@ fn write_minimal_rollout_with_id_in_subdir(codex_home: &Path, subdir: &str, id:
|
||||
})
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
file
|
||||
}
|
||||
|
||||
/// Create sessions/YYYY/MM/DD and write a minimal rollout file containing the
|
||||
@@ -56,7 +61,11 @@ fn write_minimal_rollout_with_id(codex_home: &Path, id: Uuid) -> PathBuf {
|
||||
write_minimal_rollout_with_id_in_subdir(codex_home, "sessions", id)
|
||||
}
|
||||
|
||||
async fn upsert_thread_metadata(codex_home: &Path, thread_id: ThreadId, rollout_path: PathBuf) {
|
||||
async fn upsert_thread_metadata(
|
||||
codex_home: &Path,
|
||||
thread_id: ThreadId,
|
||||
rollout_path: PathBuf,
|
||||
) -> StateDbHandle {
|
||||
let runtime = StateRuntime::init(codex_home.to_path_buf(), "test-provider".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -73,6 +82,7 @@ async fn upsert_thread_metadata(codex_home: &Path, thread_id: ThreadId, rollout_
|
||||
builder.cwd = codex_home.to_path_buf();
|
||||
let metadata = builder.build("test-provider");
|
||||
runtime.upsert_thread(&metadata).await.unwrap();
|
||||
runtime
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -81,9 +91,10 @@ 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())
|
||||
.await
|
||||
.unwrap();
|
||||
let found =
|
||||
find_thread_path_by_id_str(home.path(), &id.to_string(), /*state_db_ctx*/ None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(found.unwrap(), expected);
|
||||
}
|
||||
@@ -97,9 +108,10 @@ 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())
|
||||
.await
|
||||
.unwrap();
|
||||
let found =
|
||||
find_thread_path_by_id_str(&codex_home, &id.to_string(), /*state_db_ctx*/ None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(found, Some(expected));
|
||||
}
|
||||
@@ -113,11 +125,11 @@ async fn find_prefers_sqlite_path_by_id() {
|
||||
"sessions/2030/12/30/rollout-2030-12-30T00-00-00-{id}.jsonl"
|
||||
));
|
||||
std::fs::create_dir_all(db_path.parent().unwrap()).unwrap();
|
||||
std::fs::write(&db_path, "").unwrap();
|
||||
write_minimal_rollout_with_id_at_path(&db_path, id);
|
||||
write_minimal_rollout_with_id(home.path(), id);
|
||||
upsert_thread_metadata(home.path(), thread_id, db_path.clone()).await;
|
||||
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())
|
||||
let found = find_thread_path_by_id_str(home.path(), &id.to_string(), Some(&state_db))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -134,9 +146,9 @@ async fn find_falls_back_to_filesystem_when_sqlite_has_no_match() {
|
||||
let unrelated_path = home
|
||||
.path()
|
||||
.join("sessions/2030/12/30/rollout-2030-12-30T00-00-00-unrelated.jsonl");
|
||||
upsert_thread_metadata(home.path(), unrelated_thread_id, unrelated_path).await;
|
||||
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())
|
||||
let found = find_thread_path_by_id_str(home.path(), &id.to_string(), Some(&state_db))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -150,9 +162,10 @@ 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())
|
||||
.await
|
||||
.unwrap();
|
||||
let found =
|
||||
find_thread_path_by_id_str(home.path(), &id.to_string(), /*state_db_ctx*/ None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(found, Some(expected));
|
||||
}
|
||||
@@ -197,7 +210,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).await?;
|
||||
let found =
|
||||
find_thread_meta_by_name_str(home.path(), thread_name, /*state_db_ctx*/ None).await?;
|
||||
|
||||
let (path, session_meta) = found.expect("expected rollout path to be found");
|
||||
assert_eq!(session_meta.meta.id, thread_id);
|
||||
@@ -214,9 +228,13 @@ async fn find_archived_locates_rollout_file_by_id() {
|
||||
let id = Uuid::new_v4();
|
||||
let expected = write_minimal_rollout_with_id_in_subdir(home.path(), "archived_sessions", id);
|
||||
|
||||
let found = find_archived_thread_path_by_id_str(home.path(), &id.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let found = find_archived_thread_path_by_id_str(
|
||||
home.path(),
|
||||
&id.to_string(),
|
||||
/*state_db_ctx*/ None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(found, Some(expected));
|
||||
}
|
||||
|
||||
@@ -246,7 +246,8 @@ async fn list_skills_skips_cwd_roots_when_environment_disabled() -> Result<()> {
|
||||
)?,
|
||||
)),
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(&config),
|
||||
thread_store_from_config(&config, /*state_db*/ None),
|
||||
/*state_db*/ None,
|
||||
);
|
||||
let new_thread = thread_manager.start_thread(config.clone()).await?;
|
||||
let cwd = config.cwd.to_path_buf();
|
||||
|
||||
@@ -8,13 +8,9 @@ use tokio::sync::watch;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
#[cfg(test)]
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
#[cfg(test)]
|
||||
use tokio::io::AsyncWriteExt;
|
||||
#[cfg(test)]
|
||||
use tokio::io::BufReader;
|
||||
#[cfg(test)]
|
||||
use tokio::io::BufWriter;
|
||||
|
||||
pub(crate) const CHANNEL_CAPACITY: usize = 128;
|
||||
@@ -34,7 +30,6 @@ pub(crate) struct JsonRpcConnection {
|
||||
}
|
||||
|
||||
impl JsonRpcConnection {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn from_stdio<R, W>(reader: R, writer: W, connection_label: String) -> Self
|
||||
where
|
||||
R: AsyncRead + Unpin + Send + 'static,
|
||||
@@ -298,7 +293,6 @@ async fn send_malformed_message(
|
||||
.await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn write_jsonrpc_line_message<W>(
|
||||
writer: &mut BufWriter<W>,
|
||||
message: &JSONRPCMessage,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use std::io::Write as _;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::io;
|
||||
use tokio::io::AsyncRead;
|
||||
use tokio::io::AsyncWrite;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_tungstenite::accept_async;
|
||||
use tracing::warn;
|
||||
@@ -10,6 +13,12 @@ use crate::server::processor::ConnectionProcessor;
|
||||
|
||||
pub const DEFAULT_LISTEN_URL: &str = "ws://127.0.0.1:0";
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub(crate) enum ExecServerListenTransport {
|
||||
WebSocket(SocketAddr),
|
||||
Stdio,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum ExecServerListenUrlParseError {
|
||||
UnsupportedListenUrl(String),
|
||||
@@ -21,7 +30,7 @@ impl std::fmt::Display for ExecServerListenUrlParseError {
|
||||
match self {
|
||||
ExecServerListenUrlParseError::UnsupportedListenUrl(listen_url) => write!(
|
||||
f,
|
||||
"unsupported --listen URL `{listen_url}`; expected `ws://IP:PORT`"
|
||||
"unsupported --listen URL `{listen_url}`; expected `ws://IP:PORT` or `stdio`"
|
||||
),
|
||||
ExecServerListenUrlParseError::InvalidWebSocketListenUrl(listen_url) => write!(
|
||||
f,
|
||||
@@ -35,11 +44,18 @@ impl std::error::Error for ExecServerListenUrlParseError {}
|
||||
|
||||
pub(crate) fn parse_listen_url(
|
||||
listen_url: &str,
|
||||
) -> Result<SocketAddr, ExecServerListenUrlParseError> {
|
||||
) -> Result<ExecServerListenTransport, ExecServerListenUrlParseError> {
|
||||
if matches!(listen_url, "stdio" | "stdio://") {
|
||||
return Ok(ExecServerListenTransport::Stdio);
|
||||
}
|
||||
|
||||
if let Some(socket_addr) = listen_url.strip_prefix("ws://") {
|
||||
return socket_addr.parse::<SocketAddr>().map_err(|_| {
|
||||
ExecServerListenUrlParseError::InvalidWebSocketListenUrl(listen_url.to_string())
|
||||
});
|
||||
return socket_addr
|
||||
.parse::<SocketAddr>()
|
||||
.map(ExecServerListenTransport::WebSocket)
|
||||
.map_err(|_| {
|
||||
ExecServerListenUrlParseError::InvalidWebSocketListenUrl(listen_url.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
Err(ExecServerListenUrlParseError::UnsupportedListenUrl(
|
||||
@@ -51,8 +67,39 @@ pub(crate) async fn run_transport(
|
||||
listen_url: &str,
|
||||
runtime_paths: ExecServerRuntimePaths,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let bind_address = parse_listen_url(listen_url)?;
|
||||
run_websocket_listener(bind_address, runtime_paths).await
|
||||
match parse_listen_url(listen_url)? {
|
||||
ExecServerListenTransport::WebSocket(bind_address) => {
|
||||
run_websocket_listener(bind_address, runtime_paths).await
|
||||
}
|
||||
ExecServerListenTransport::Stdio => run_stdio_connection(runtime_paths).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_stdio_connection(
|
||||
runtime_paths: ExecServerRuntimePaths,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
run_stdio_connection_with_io(io::stdin(), io::stdout(), runtime_paths).await
|
||||
}
|
||||
|
||||
async fn run_stdio_connection_with_io<R, W>(
|
||||
reader: R,
|
||||
writer: W,
|
||||
runtime_paths: ExecServerRuntimePaths,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
|
||||
where
|
||||
R: AsyncRead + Unpin + Send + 'static,
|
||||
W: AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
let processor = ConnectionProcessor::new(runtime_paths);
|
||||
tracing::info!("codex-exec-server listening on stdio");
|
||||
processor
|
||||
.run_connection(JsonRpcConnection::from_stdio(
|
||||
reader,
|
||||
writer,
|
||||
"exec-server stdio".to_string(),
|
||||
))
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_websocket_listener(
|
||||
|
||||
@@ -1,31 +1,127 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::JSONRPCRequest;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::io::BufReader;
|
||||
use tokio::io::duplex;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use super::DEFAULT_LISTEN_URL;
|
||||
use super::ExecServerListenTransport;
|
||||
use super::parse_listen_url;
|
||||
use super::run_stdio_connection_with_io;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::protocol::INITIALIZE_METHOD;
|
||||
use crate::protocol::INITIALIZED_METHOD;
|
||||
use crate::protocol::InitializeParams;
|
||||
use crate::protocol::InitializeResponse;
|
||||
|
||||
#[test]
|
||||
fn parse_listen_url_accepts_default_websocket_url() {
|
||||
let bind_address =
|
||||
parse_listen_url(DEFAULT_LISTEN_URL).expect("default listen URL should parse");
|
||||
let transport = parse_listen_url(DEFAULT_LISTEN_URL).expect("default listen URL should parse");
|
||||
assert_eq!(
|
||||
bind_address,
|
||||
"127.0.0.1:0"
|
||||
.parse::<SocketAddr>()
|
||||
.expect("valid socket address")
|
||||
transport,
|
||||
ExecServerListenTransport::WebSocket(
|
||||
"127.0.0.1:0"
|
||||
.parse::<SocketAddr>()
|
||||
.expect("valid socket address")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_listen_url_accepts_stdio() {
|
||||
let transport = parse_listen_url("stdio").expect("stdio listen URL should parse");
|
||||
assert_eq!(transport, ExecServerListenTransport::Stdio);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_listen_url_accepts_stdio_url() {
|
||||
let transport = parse_listen_url("stdio://").expect("stdio listen URL should parse");
|
||||
assert_eq!(transport, ExecServerListenTransport::Stdio);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stdio_listen_transport_serves_initialize() {
|
||||
let transport = parse_listen_url("stdio").expect("stdio listen URL should parse");
|
||||
let ExecServerListenTransport::Stdio = transport else {
|
||||
panic!("expected stdio listen transport, got {transport:?}");
|
||||
};
|
||||
|
||||
let (mut client_writer, server_reader) = duplex(1 << 20);
|
||||
let (server_writer, client_reader) = duplex(1 << 20);
|
||||
let server_task = tokio::spawn(run_stdio_connection_with_io(
|
||||
server_reader,
|
||||
server_writer,
|
||||
test_runtime_paths(),
|
||||
));
|
||||
let mut client_lines = BufReader::new(client_reader).lines();
|
||||
|
||||
let initialize = JSONRPCMessage::Request(JSONRPCRequest {
|
||||
id: RequestId::Integer(1),
|
||||
method: INITIALIZE_METHOD.to_string(),
|
||||
params: Some(
|
||||
serde_json::to_value(InitializeParams {
|
||||
client_name: "exec-server-transport-test".to_string(),
|
||||
resume_session_id: None,
|
||||
})
|
||||
.expect("initialize params should serialize"),
|
||||
),
|
||||
trace: None,
|
||||
});
|
||||
write_jsonrpc_line(&mut client_writer, &initialize).await;
|
||||
|
||||
let response = timeout(Duration::from_secs(1), client_lines.next_line())
|
||||
.await
|
||||
.expect("initialize response should arrive")
|
||||
.expect("initialize response read should succeed")
|
||||
.expect("initialize response should be present");
|
||||
let response: JSONRPCMessage =
|
||||
serde_json::from_str(&response).expect("initialize response should parse");
|
||||
let JSONRPCMessage::Response(JSONRPCResponse { id, result }) = response else {
|
||||
panic!("expected initialize response, got {response:?}");
|
||||
};
|
||||
assert_eq!(id, RequestId::Integer(1));
|
||||
let initialize_response: InitializeResponse =
|
||||
serde_json::from_value(result).expect("initialize response should decode");
|
||||
assert!(
|
||||
!initialize_response.session_id.is_empty(),
|
||||
"initialize should return a session id"
|
||||
);
|
||||
|
||||
let initialized = JSONRPCMessage::Notification(JSONRPCNotification {
|
||||
method: INITIALIZED_METHOD.to_string(),
|
||||
params: Some(serde_json::to_value(()).expect("initialized params should serialize")),
|
||||
});
|
||||
write_jsonrpc_line(&mut client_writer, &initialized).await;
|
||||
|
||||
drop(client_writer);
|
||||
drop(client_lines);
|
||||
timeout(Duration::from_secs(1), server_task)
|
||||
.await
|
||||
.expect("stdio transport should finish after client disconnect")
|
||||
.expect("stdio transport task should join")
|
||||
.expect("stdio transport should not fail");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_listen_url_accepts_websocket_url() {
|
||||
let bind_address =
|
||||
let transport =
|
||||
parse_listen_url("ws://127.0.0.1:1234").expect("websocket listen URL should parse");
|
||||
assert_eq!(
|
||||
bind_address,
|
||||
"127.0.0.1:1234"
|
||||
.parse::<SocketAddr>()
|
||||
.expect("valid socket address")
|
||||
transport,
|
||||
ExecServerListenTransport::WebSocket(
|
||||
"127.0.0.1:1234"
|
||||
.parse::<SocketAddr>()
|
||||
.expect("valid socket address")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,6 +141,26 @@ fn parse_listen_url_rejects_unsupported_url() {
|
||||
parse_listen_url("http://127.0.0.1:1234").expect_err("unsupported scheme should fail");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"unsupported --listen URL `http://127.0.0.1:1234`; expected `ws://IP:PORT`"
|
||||
"unsupported --listen URL `http://127.0.0.1:1234`; expected `ws://IP:PORT` or `stdio`"
|
||||
);
|
||||
}
|
||||
|
||||
async fn write_jsonrpc_line(writer: &mut tokio::io::DuplexStream, message: &JSONRPCMessage) {
|
||||
let encoded = serde_json::to_vec(message).expect("JSON-RPC message should serialize");
|
||||
writer
|
||||
.write_all(&encoded)
|
||||
.await
|
||||
.expect("JSON-RPC message should write");
|
||||
writer
|
||||
.write_all(b"\n")
|
||||
.await
|
||||
.expect("JSON-RPC newline should write");
|
||||
}
|
||||
|
||||
fn test_runtime_paths() -> ExecServerRuntimePaths {
|
||||
ExecServerRuntimePaths::new(
|
||||
std::env::current_exe().expect("current exe"),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("runtime paths")
|
||||
}
|
||||
|
||||
@@ -57,6 +57,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::StateDbHandle;
|
||||
use codex_core::check_execpolicy_for_warnings;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
@@ -194,6 +195,7 @@ impl RequestIdSequencer {
|
||||
|
||||
struct ExecRunArgs {
|
||||
in_process_start_args: InProcessClientStartArgs,
|
||||
state_db: Option<StateDbHandle>,
|
||||
command: Option<ExecCommand>,
|
||||
config: Config,
|
||||
dangerously_bypass_approvals_and_sandbox: bool,
|
||||
@@ -503,6 +505,7 @@ 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 in_process_start_args = InProcessClientStartArgs {
|
||||
arg0_paths,
|
||||
config: std::sync::Arc::new(config.clone()),
|
||||
@@ -511,6 +514,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(),
|
||||
environment_manager: std::sync::Arc::new(
|
||||
EnvironmentManager::new(EnvironmentManagerArgs::new(local_runtime_paths)).await,
|
||||
),
|
||||
@@ -525,6 +529,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
};
|
||||
run_exec_session(ExecRunArgs {
|
||||
in_process_start_args,
|
||||
state_db,
|
||||
command,
|
||||
config,
|
||||
dangerously_bypass_approvals_and_sandbox,
|
||||
@@ -546,6 +551,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
let ExecRunArgs {
|
||||
in_process_start_args,
|
||||
state_db,
|
||||
command,
|
||||
config,
|
||||
dangerously_bypass_approvals_and_sandbox,
|
||||
@@ -672,7 +678,9 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
let (primary_thread_id, fallback_session_configured) = if let Some(ExecCommand::Resume(args)) =
|
||||
command.as_ref()
|
||||
{
|
||||
if let Some(thread_id) = resolve_resume_thread_id(&client, &config, args).await? {
|
||||
if let Some(thread_id) =
|
||||
resolve_resume_thread_id(&client, &config, state_db.as_ref(), args).await?
|
||||
{
|
||||
let response: ThreadResumeResponse = send_request_with_response(
|
||||
&client,
|
||||
ClientRequest::ThreadResume {
|
||||
@@ -1318,6 +1326,7 @@ fn cwds_match(current_cwd: &Path, session_cwd: &Path) -> bool {
|
||||
async fn resolve_resume_thread_id(
|
||||
client: &InProcessAppServerClient,
|
||||
config: &Config,
|
||||
state_db: Option<&StateDbHandle>,
|
||||
args: &crate::cli::ResumeArgs,
|
||||
) -> anyhow::Result<Option<String>> {
|
||||
let model_providers = resume_lookup_model_providers(config, args);
|
||||
@@ -1365,7 +1374,7 @@ async fn resolve_resume_thread_id(
|
||||
if Uuid::parse_str(session_id).is_ok() {
|
||||
return Ok(Some(session_id.to_string()));
|
||||
}
|
||||
if let Some(state_db) = codex_core::get_state_db(config).await {
|
||||
if let Some(state_db) = state_db {
|
||||
let cwd = (!args.all).then_some(config.cwd.as_path());
|
||||
let resolved = state_db
|
||||
.find_thread_by_exact_title(
|
||||
@@ -1380,7 +1389,8 @@ async fn resolve_resume_thread_id(
|
||||
return Ok(Some(thread.id.to_string()));
|
||||
}
|
||||
if let Some((_, session_meta)) =
|
||||
find_thread_meta_by_name_str(&config.codex_home, session_id).await?
|
||||
find_thread_meta_by_name_str(&config.codex_home, session_id, Some(state_db.as_ref()))
|
||||
.await?
|
||||
&& (args.all || cwds_match(config.cwd.as_path(), &session_meta.meta.cwd))
|
||||
{
|
||||
return Ok(Some(session_meta.meta.id.to_string()));
|
||||
|
||||
@@ -83,6 +83,7 @@ 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 otel = codex_core::otel_init::build_provider(
|
||||
&config,
|
||||
@@ -144,6 +145,7 @@ pub async fn run_main(
|
||||
arg0_paths,
|
||||
Arc::new(config),
|
||||
environment_manager,
|
||||
state_db,
|
||||
)
|
||||
.await;
|
||||
async move {
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_core::StateDbHandle;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::thread_store_from_config;
|
||||
@@ -53,6 +54,7 @@ impl MessageProcessor {
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
config: Arc<Config>,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) -> Self {
|
||||
let outgoing = Arc::new(outgoing);
|
||||
let auth_manager = AuthManager::shared_from_config(
|
||||
@@ -66,7 +68,8 @@ impl MessageProcessor {
|
||||
SessionSource::Mcp,
|
||||
environment_manager,
|
||||
/*analytics_events_client*/ None,
|
||||
thread_store_from_config(config.as_ref()),
|
||||
thread_store_from_config(config.as_ref(), state_db.clone()),
|
||||
state_db.clone(),
|
||||
));
|
||||
Self {
|
||||
outgoing,
|
||||
|
||||
@@ -1239,6 +1239,7 @@ async fn find_thread_path_by_id_str_in_subdir(
|
||||
codex_home: &Path,
|
||||
subdir: &str,
|
||||
id_str: &str,
|
||||
state_db_ctx: Option<&codex_state::StateRuntime>,
|
||||
) -> io::Result<Option<PathBuf>> {
|
||||
// Validate UUID format early.
|
||||
if Uuid::parse_str(id_str).is_err() {
|
||||
@@ -1253,8 +1254,8 @@ async fn find_thread_path_by_id_str_in_subdir(
|
||||
_ => None,
|
||||
};
|
||||
let thread_id = ThreadId::from_string(id_str).ok();
|
||||
let state_db_ctx = state_db::open_if_present(codex_home, "").await;
|
||||
if let Some(state_db_ctx) = state_db_ctx.as_deref()
|
||||
let mut unverified_db_path = None;
|
||||
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),
|
||||
@@ -1265,21 +1266,43 @@ async fn find_thread_path_by_id_str_in_subdir(
|
||||
.await
|
||||
{
|
||||
if tokio::fs::try_exists(&db_path).await.unwrap_or(false) {
|
||||
return Ok(Some(db_path));
|
||||
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"
|
||||
);
|
||||
}
|
||||
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 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"
|
||||
);
|
||||
}
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
let mut root = codex_home.to_path_buf();
|
||||
root.push(subdir);
|
||||
if !root.exists() {
|
||||
return Ok(None);
|
||||
return Ok(unverified_db_path);
|
||||
}
|
||||
// This is safe because we know the values are valid.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
@@ -1301,7 +1324,7 @@ async fn find_thread_path_by_id_str_in_subdir(
|
||||
"state db discrepancy during find_thread_path_by_id_str_in_subdir: falling_back"
|
||||
);
|
||||
state_db::read_repair_rollout_path(
|
||||
state_db_ctx.as_deref(),
|
||||
state_db_ctx,
|
||||
thread_id,
|
||||
archived_only,
|
||||
found_path.as_path(),
|
||||
@@ -1309,7 +1332,7 @@ async fn find_thread_path_by_id_str_in_subdir(
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(found)
|
||||
Ok(found.or(unverified_db_path))
|
||||
}
|
||||
|
||||
/// Locate a recorded thread rollout file by its UUID string using the existing
|
||||
@@ -1318,16 +1341,19 @@ 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>,
|
||||
) -> io::Result<Option<PathBuf>> {
|
||||
find_thread_path_by_id_str_in_subdir(codex_home, SESSIONS_SUBDIR, id_str).await
|
||||
find_thread_path_by_id_str_in_subdir(codex_home, SESSIONS_SUBDIR, id_str, state_db_ctx).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>,
|
||||
) -> io::Result<Option<PathBuf>> {
|
||||
find_thread_path_by_id_str_in_subdir(codex_home, ARCHIVED_SESSIONS_SUBDIR, id_str).await
|
||||
find_thread_path_by_id_str_in_subdir(codex_home, ARCHIVED_SESSIONS_SUBDIR, id_str, state_db_ctx)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Extract the `YYYY/MM/DD` directory components from a rollout filename.
|
||||
|
||||
@@ -136,6 +136,21 @@ pub(crate) async fn backfill_sessions(
|
||||
runtime: &codex_state::StateRuntime,
|
||||
codex_home: &Path,
|
||||
default_provider: &str,
|
||||
) {
|
||||
backfill_sessions_with_lease(
|
||||
runtime,
|
||||
codex_home,
|
||||
default_provider,
|
||||
BACKFILL_LEASE_SECONDS,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn backfill_sessions_with_lease(
|
||||
runtime: &codex_state::StateRuntime,
|
||||
codex_home: &Path,
|
||||
default_provider: &str,
|
||||
backfill_lease_seconds: i64,
|
||||
) {
|
||||
let metric_client = codex_otel::global();
|
||||
let timer = metric_client
|
||||
@@ -154,7 +169,7 @@ pub(crate) async fn backfill_sessions(
|
||||
if backfill_state.status == BackfillStatus::Complete {
|
||||
return;
|
||||
}
|
||||
let claimed = match runtime.try_claim_backfill(BACKFILL_LEASE_SECONDS).await {
|
||||
let claimed = match runtime.try_claim_backfill(backfill_lease_seconds).await {
|
||||
Ok(claimed) => claimed,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
|
||||
@@ -102,10 +102,12 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option<EventPersistenceMode> {
|
||||
| EventMsg::ContextCompacted(_)
|
||||
| EventMsg::EnteredReviewMode(_)
|
||||
| EventMsg::ExitedReviewMode(_)
|
||||
| EventMsg::McpToolCallEnd(_)
|
||||
| EventMsg::ThreadRolledBack(_)
|
||||
| EventMsg::TurnAborted(_)
|
||||
| EventMsg::TurnStarted(_)
|
||||
| EventMsg::TurnComplete(_)
|
||||
| EventMsg::WebSearchEnd(_)
|
||||
| EventMsg::ImageGenerationEnd(_) => Some(EventPersistenceMode::Limited),
|
||||
EventMsg::ItemCompleted(event) => {
|
||||
// Plan items are derived from streaming tags and are not part of the
|
||||
@@ -117,14 +119,9 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option<EventPersistenceMode> {
|
||||
None
|
||||
}
|
||||
}
|
||||
EventMsg::McpToolCallEnd(event) if event.mcp_app_resource_uri.is_some() => {
|
||||
Some(EventPersistenceMode::Limited)
|
||||
}
|
||||
EventMsg::Error(_)
|
||||
| EventMsg::GuardianAssessment(_)
|
||||
| EventMsg::WebSearchEnd(_)
|
||||
| EventMsg::ExecCommandEnd(_)
|
||||
| EventMsg::McpToolCallEnd(_)
|
||||
| EventMsg::ViewImageToolCall(_)
|
||||
| EventMsg::CollabAgentSpawnEnd(_)
|
||||
| EventMsg::CollabAgentInteractionEnd(_)
|
||||
|
||||
@@ -79,7 +79,6 @@ pub struct RolloutRecorder {
|
||||
tx: Sender<RolloutCmd>,
|
||||
writer_task: Arc<RolloutWriterTask>,
|
||||
pub(crate) rollout_path: PathBuf,
|
||||
state_db: Option<StateDbHandle>,
|
||||
event_persistence_mode: EventPersistenceMode,
|
||||
}
|
||||
|
||||
@@ -230,6 +229,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>,
|
||||
config: &impl RolloutConfigView,
|
||||
page_size: usize,
|
||||
cursor: Option<&Cursor>,
|
||||
@@ -242,6 +242,7 @@ impl RolloutRecorder {
|
||||
search_term: Option<&str>,
|
||||
) -> std::io::Result<ThreadsPage> {
|
||||
Self::list_threads_with_db_fallback(
|
||||
state_db_ctx,
|
||||
config,
|
||||
page_size,
|
||||
cursor,
|
||||
@@ -260,6 +261,7 @@ impl RolloutRecorder {
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn list_threads_from_state_db(
|
||||
state_db_ctx: Option<StateDbHandle>,
|
||||
config: &impl RolloutConfigView,
|
||||
page_size: usize,
|
||||
cursor: Option<&Cursor>,
|
||||
@@ -272,6 +274,7 @@ impl RolloutRecorder {
|
||||
search_term: Option<&str>,
|
||||
) -> std::io::Result<ThreadsPage> {
|
||||
Self::list_threads_with_db_fallback(
|
||||
state_db_ctx,
|
||||
config,
|
||||
page_size,
|
||||
cursor,
|
||||
@@ -291,6 +294,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>,
|
||||
config: &impl RolloutConfigView,
|
||||
page_size: usize,
|
||||
cursor: Option<&Cursor>,
|
||||
@@ -303,6 +307,7 @@ impl RolloutRecorder {
|
||||
search_term: Option<&str>,
|
||||
) -> std::io::Result<ThreadsPage> {
|
||||
Self::list_threads_with_db_fallback(
|
||||
state_db_ctx,
|
||||
config,
|
||||
page_size,
|
||||
cursor,
|
||||
@@ -321,6 +326,7 @@ impl RolloutRecorder {
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn list_archived_threads_from_state_db(
|
||||
state_db_ctx: Option<StateDbHandle>,
|
||||
config: &impl RolloutConfigView,
|
||||
page_size: usize,
|
||||
cursor: Option<&Cursor>,
|
||||
@@ -333,6 +339,7 @@ impl RolloutRecorder {
|
||||
search_term: Option<&str>,
|
||||
) -> std::io::Result<ThreadsPage> {
|
||||
Self::list_threads_with_db_fallback(
|
||||
state_db_ctx,
|
||||
config,
|
||||
page_size,
|
||||
cursor,
|
||||
@@ -351,6 +358,7 @@ impl RolloutRecorder {
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn list_threads_with_db_fallback(
|
||||
state_db_ctx: Option<StateDbHandle>,
|
||||
config: &impl RolloutConfigView,
|
||||
page_size: usize,
|
||||
cursor: Option<&Cursor>,
|
||||
@@ -365,7 +373,6 @@ impl RolloutRecorder {
|
||||
search_term: Option<&str>,
|
||||
) -> std::io::Result<ThreadsPage> {
|
||||
let codex_home = config.codex_home();
|
||||
let state_db_ctx = state_db::get_state_db(config).await;
|
||||
let archived = match archive_filter {
|
||||
ThreadListArchiveFilter::Active => false,
|
||||
ThreadListArchiveFilter::Archived => true,
|
||||
@@ -575,6 +582,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>,
|
||||
config: &impl RolloutConfigView,
|
||||
page_size: usize,
|
||||
cursor: Option<&Cursor>,
|
||||
@@ -585,7 +593,6 @@ impl RolloutRecorder {
|
||||
filter_cwd: Option<&Path>,
|
||||
) -> std::io::Result<Option<PathBuf>> {
|
||||
let codex_home = config.codex_home();
|
||||
let state_db_ctx = state_db::get_state_db(config).await;
|
||||
let cwd_filter = filter_cwd.map(Path::to_path_buf);
|
||||
if state_db_ctx.is_some() {
|
||||
let mut db_cursor = cursor.cloned();
|
||||
@@ -770,7 +777,6 @@ impl RolloutRecorder {
|
||||
tx,
|
||||
writer_task,
|
||||
rollout_path,
|
||||
state_db: state_db_ctx,
|
||||
event_persistence_mode,
|
||||
})
|
||||
}
|
||||
@@ -779,10 +785,6 @@ impl RolloutRecorder {
|
||||
self.rollout_path.as_path()
|
||||
}
|
||||
|
||||
pub fn state_db(&self) -> Option<StateDbHandle> {
|
||||
self.state_db.clone()
|
||||
}
|
||||
|
||||
pub async fn record_items(&self, items: &[RolloutItem]) -> std::io::Result<()> {
|
||||
let mut filtered = Vec::new();
|
||||
for item in items {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use super::*;
|
||||
use crate::config::RolloutConfig;
|
||||
use chrono::TimeZone;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::AgentMessageEvent;
|
||||
@@ -11,6 +12,9 @@ use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionMeta;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
use codex_protocol::protocol::UserMessageEvent;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -65,6 +69,77 @@ fn write_session_file(root: &Path, ts: &str, uuid: Uuid) -> std::io::Result<Path
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn state_db_init_backfills_before_returning() -> anyhow::Result<()> {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let uuid = Uuid::new_v4();
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
||||
let rollout_path = home.path().join(format!(
|
||||
"sessions/2026/01/27/rollout-2026-01-27T12-34-56-{uuid}.jsonl"
|
||||
));
|
||||
let parent = rollout_path
|
||||
.parent()
|
||||
.expect("rollout path should have parent");
|
||||
fs::create_dir_all(parent)?;
|
||||
|
||||
let session_meta_line = SessionMetaLine {
|
||||
meta: SessionMeta {
|
||||
id: thread_id,
|
||||
forked_from_id: None,
|
||||
timestamp: "2026-01-27T12:34:56Z".to_string(),
|
||||
cwd: home.path().to_path_buf(),
|
||||
originator: "test".to_string(),
|
||||
cli_version: "test".to_string(),
|
||||
source: SessionSource::Cli,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
},
|
||||
git: None,
|
||||
};
|
||||
let lines = [
|
||||
RolloutLine {
|
||||
timestamp: "2026-01-27T12:34:56Z".to_string(),
|
||||
item: RolloutItem::SessionMeta(session_meta_line),
|
||||
},
|
||||
RolloutLine {
|
||||
timestamp: "2026-01-27T12:34:57Z".to_string(),
|
||||
item: RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "hello from startup backfill".to_string(),
|
||||
images: None,
|
||||
local_images: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
})),
|
||||
},
|
||||
];
|
||||
let jsonl = lines
|
||||
.iter()
|
||||
.map(serde_json::to_string)
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.join("\n");
|
||||
fs::write(&rollout_path, format!("{jsonl}\n"))?;
|
||||
|
||||
let runtime = crate::state_db::init(&test_config(home.path()))
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
|
||||
let metadata = runtime
|
||||
.get_thread(thread_id)
|
||||
.await?
|
||||
.expect("thread should be backfilled before init returns");
|
||||
assert_eq!(metadata.rollout_path, rollout_path);
|
||||
assert_eq!(
|
||||
runtime.get_backfill_state().await?.status,
|
||||
codex_state::BackfillStatus::Complete
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_rollout_items_skips_legacy_ghost_snapshot_lines() -> std::io::Result<()> {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
@@ -526,6 +601,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,
|
||||
&config,
|
||||
/*page_size*/ 1,
|
||||
/*cursor*/ None,
|
||||
@@ -543,6 +619,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,
|
||||
&config,
|
||||
/*page_size*/ 1,
|
||||
Some(&cursor),
|
||||
@@ -602,6 +679,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()),
|
||||
&config,
|
||||
/*page_size*/ 10,
|
||||
/*cursor*/ None,
|
||||
@@ -666,6 +744,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()),
|
||||
&config,
|
||||
/*page_size*/ 1,
|
||||
/*cursor*/ None,
|
||||
@@ -738,6 +817,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()),
|
||||
&config,
|
||||
/*page_size*/ 10,
|
||||
/*cursor*/ None,
|
||||
@@ -753,6 +833,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()),
|
||||
&config,
|
||||
/*page_size*/ 10,
|
||||
/*cursor*/ None,
|
||||
@@ -768,6 +849,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()),
|
||||
&config,
|
||||
/*page_size*/ 10,
|
||||
/*cursor*/ None,
|
||||
@@ -825,6 +907,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()),
|
||||
&config,
|
||||
/*page_size*/ 10,
|
||||
/*cursor*/ None,
|
||||
@@ -840,6 +923,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()),
|
||||
&config,
|
||||
/*page_size*/ 10,
|
||||
/*cursor*/ None,
|
||||
@@ -855,6 +939,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()),
|
||||
&config,
|
||||
/*page_size*/ 10,
|
||||
/*cursor*/ None,
|
||||
@@ -913,6 +998,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()),
|
||||
&config,
|
||||
/*page_size*/ 10,
|
||||
/*cursor*/ None,
|
||||
@@ -1039,6 +1125,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()),
|
||||
&config,
|
||||
/*page_size*/ 10,
|
||||
/*cursor*/ None,
|
||||
@@ -1054,6 +1141,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()),
|
||||
&config,
|
||||
/*page_size*/ 10,
|
||||
/*cursor*/ None,
|
||||
@@ -1069,6 +1157,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()),
|
||||
&config,
|
||||
/*page_size*/ 10,
|
||||
/*cursor*/ None,
|
||||
|
||||
@@ -117,6 +117,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>,
|
||||
) -> std::io::Result<Option<(PathBuf, SessionMetaLine)>> {
|
||||
if name.trim().is_empty() {
|
||||
return Ok(None);
|
||||
@@ -135,8 +136,12 @@ pub async fn find_thread_meta_by_name_str(
|
||||
while let Some(thread_id) = rx.recv().await {
|
||||
// Keep walking until a matching id resolves to a loadable rollout so an unsaved or partial
|
||||
// rename cannot shadow an older persisted session with the same name.
|
||||
if let Some(path) =
|
||||
super::list::find_thread_path_by_id_str(codex_home, &thread_id.to_string()).await?
|
||||
if let Some(path) = super::list::find_thread_path_by_id_str(
|
||||
codex_home,
|
||||
&thread_id.to_string(),
|
||||
state_db_ctx,
|
||||
)
|
||||
.await?
|
||||
&& let Ok(session_meta) = super::list::read_session_meta_line(&path).await
|
||||
{
|
||||
drop(rx);
|
||||
|
||||
@@ -99,7 +99,7 @@ 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").await?;
|
||||
let found = find_thread_meta_by_name_str(temp.path(), "same", /*state_db_ctx*/ None).await?;
|
||||
|
||||
assert_eq!(
|
||||
found.map(|(path, session_meta)| (path, session_meta.meta.id)),
|
||||
@@ -136,7 +136,7 @@ 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").await?;
|
||||
let found = find_thread_meta_by_name_str(temp.path(), "same", /*state_db_ctx*/ None).await?;
|
||||
|
||||
assert_eq!(found.map(|(path, _)| path), Some(saved_rollout_path));
|
||||
Ok(())
|
||||
@@ -174,7 +174,7 @@ 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").await?;
|
||||
let found = find_thread_meta_by_name_str(temp.path(), "same", /*state_db_ctx*/ None).await?;
|
||||
|
||||
assert_eq!(found.map(|(path, _)| path), Some(current_rollout_path));
|
||||
Ok(())
|
||||
|
||||
@@ -17,15 +17,52 @@ use serde_json::Value;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use tracing::info;
|
||||
use tracing::warn;
|
||||
|
||||
/// Core-facing handle to the SQLite-backed state runtime.
|
||||
pub type StateDbHandle = Arc<codex_state::StateRuntime>;
|
||||
|
||||
/// Initialize the state runtime for thread state persistence and backfill checks.
|
||||
#[cfg(not(test))]
|
||||
const STARTUP_BACKFILL_POLL_INTERVAL: Duration = Duration::from_secs(1);
|
||||
#[cfg(test)]
|
||||
const STARTUP_BACKFILL_POLL_INTERVAL: Duration = Duration::from_millis(10);
|
||||
#[cfg(not(test))]
|
||||
const STARTUP_BACKFILL_WAIT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
#[cfg(test)]
|
||||
const STARTUP_BACKFILL_WAIT_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
/// Initialize the state runtime for thread state persistence.
|
||||
///
|
||||
/// 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> {
|
||||
let config = RolloutConfig::from_view(config);
|
||||
init_with_roots(
|
||||
match try_init_with_roots(
|
||||
config.codex_home,
|
||||
config.sqlite_home,
|
||||
config.model_provider_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(runtime) => Some(runtime),
|
||||
Err(err) => {
|
||||
emit_startup_warning(&format!("failed to initialize state runtime: {err}"));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the state runtime and return any initialization error to the caller.
|
||||
///
|
||||
/// 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> {
|
||||
let config = RolloutConfig::from_view(config);
|
||||
try_init_with_roots(
|
||||
config.codex_home,
|
||||
config.sqlite_home,
|
||||
config.model_provider_id,
|
||||
@@ -33,52 +70,128 @@ pub async fn init(config: &impl RolloutConfigView) -> Option<StateDbHandle> {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Initialize the state runtime for a local thread store.
|
||||
pub async fn init_with_roots(
|
||||
async fn try_init_with_roots(
|
||||
codex_home: PathBuf,
|
||||
sqlite_home: PathBuf,
|
||||
default_model_provider_id: String,
|
||||
) -> Option<StateDbHandle> {
|
||||
let runtime = match codex_state::StateRuntime::init(
|
||||
sqlite_home.clone(),
|
||||
default_model_provider_id.clone(),
|
||||
) -> anyhow::Result<StateDbHandle> {
|
||||
try_init_with_roots_inner(
|
||||
codex_home,
|
||||
sqlite_home,
|
||||
default_model_provider_id,
|
||||
/*backfill_lease_seconds*/ None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(runtime) => runtime,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"failed to initialize state runtime at {}: {err}",
|
||||
sqlite_home.display()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let backfill_state = match runtime.get_backfill_state().await {
|
||||
Ok(state) => state,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn try_init_with_roots_and_backfill_lease(
|
||||
codex_home: PathBuf,
|
||||
sqlite_home: PathBuf,
|
||||
default_model_provider_id: String,
|
||||
backfill_lease_seconds: i64,
|
||||
) -> anyhow::Result<StateDbHandle> {
|
||||
try_init_with_roots_inner(
|
||||
codex_home,
|
||||
sqlite_home,
|
||||
default_model_provider_id,
|
||||
Some(backfill_lease_seconds),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn try_init_with_roots_inner(
|
||||
codex_home: PathBuf,
|
||||
sqlite_home: PathBuf,
|
||||
default_model_provider_id: String,
|
||||
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 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()
|
||||
);
|
||||
return None;
|
||||
)
|
||||
})?;
|
||||
if backfill_state.status == codex_state::BackfillStatus::Complete {
|
||||
return Ok(runtime);
|
||||
}
|
||||
};
|
||||
if backfill_state.status != codex_state::BackfillStatus::Complete {
|
||||
let runtime_for_backfill = runtime.clone();
|
||||
tokio::spawn(async move {
|
||||
|
||||
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(),
|
||||
backfill_lease_seconds,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
metadata::backfill_sessions(
|
||||
runtime_for_backfill.as_ref(),
|
||||
runtime.as_ref(),
|
||||
codex_home.as_path(),
|
||||
default_model_provider_id.as_str(),
|
||||
)
|
||||
.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()
|
||||
)
|
||||
})?;
|
||||
if backfill_state.status == codex_state::BackfillStatus::Complete {
|
||||
return Ok(runtime);
|
||||
}
|
||||
if wait_started.elapsed() >= STARTUP_BACKFILL_WAIT_TIMEOUT {
|
||||
return Err(anyhow::anyhow!(
|
||||
"timed out waiting for state db backfill at {} after {:?} (status: {})",
|
||||
codex_home.display(),
|
||||
STARTUP_BACKFILL_WAIT_TIMEOUT,
|
||||
backfill_state.status.as_str()
|
||||
));
|
||||
}
|
||||
|
||||
let message = format!(
|
||||
"state db backfill is {} at {}; waiting up to {:?} before retrying startup initialization",
|
||||
backfill_state.status.as_str(),
|
||||
codex_home.display(),
|
||||
STARTUP_BACKFILL_WAIT_TIMEOUT,
|
||||
);
|
||||
if reported_wait {
|
||||
info!("{message}");
|
||||
} else {
|
||||
emit_startup_warning(&message);
|
||||
reported_wait = true;
|
||||
}
|
||||
tokio::time::sleep(STARTUP_BACKFILL_POLL_INTERVAL).await;
|
||||
}
|
||||
Some(runtime)
|
||||
}
|
||||
|
||||
/// Get the DB if the feature is enabled and the DB exists.
|
||||
fn emit_startup_warning(message: &str) {
|
||||
warn!("{message}");
|
||||
if !tracing::dispatcher::has_been_set() {
|
||||
#[allow(clippy::print_stderr)]
|
||||
{
|
||||
eprintln!("{message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the DB if it exists and its startup backfill has already completed.
|
||||
///
|
||||
/// 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> {
|
||||
let state_path = codex_state::state_db_path(config.sqlite_home());
|
||||
if !tokio::fs::try_exists(&state_path).await.unwrap_or(false) {
|
||||
@@ -93,21 +206,6 @@ pub async fn get_state_db(config: &impl RolloutConfigView) -> Option<StateDbHand
|
||||
require_backfill_complete(runtime, config.sqlite_home()).await
|
||||
}
|
||||
|
||||
/// Open the state runtime when the SQLite file exists, without feature gating.
|
||||
///
|
||||
/// This is used for parity checks during the SQLite migration phase.
|
||||
pub async fn open_if_present(codex_home: &Path, default_provider: &str) -> Option<StateDbHandle> {
|
||||
let db_path = codex_state::state_db_path(codex_home);
|
||||
if !tokio::fs::try_exists(&db_path).await.unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
let runtime =
|
||||
codex_state::StateRuntime::init(codex_home.to_path_buf(), default_provider.to_string())
|
||||
.await
|
||||
.ok()?;
|
||||
require_backfill_complete(runtime, codex_home).await
|
||||
}
|
||||
|
||||
async fn require_backfill_complete(
|
||||
runtime: StateDbHandle,
|
||||
codex_home: &Path,
|
||||
|
||||
@@ -7,6 +7,7 @@ use chrono::NaiveDateTime;
|
||||
use chrono::Timelike;
|
||||
use chrono::Utc;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn cursor_to_anchor_normalizes_timestamp_format() {
|
||||
@@ -22,3 +23,64 @@ fn cursor_to_anchor_normalizes_timestamp_format() {
|
||||
|
||||
assert_eq!(anchor.ts, expected_ts);
|
||||
}
|
||||
|
||||
#[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 claimed = runtime.try_claim_backfill(/*lease_seconds*/ 60).await?;
|
||||
assert!(claimed);
|
||||
let runtime_for_completion = runtime.clone();
|
||||
let complete_backfill = tokio::spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
|
||||
runtime_for_completion
|
||||
.mark_backfill_complete(/*last_watermark*/ None)
|
||||
.await
|
||||
});
|
||||
|
||||
let initialized = try_init_with_roots_and_backfill_lease(
|
||||
home.path().to_path_buf(),
|
||||
home.path().to_path_buf(),
|
||||
"test-provider".to_string(),
|
||||
/*backfill_lease_seconds*/ 60,
|
||||
)
|
||||
.await?;
|
||||
complete_backfill.await??;
|
||||
assert_eq!(
|
||||
initialized.get_backfill_state().await?.status,
|
||||
codex_state::BackfillStatus::Complete
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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 claimed = runtime.try_claim_backfill(/*lease_seconds*/ 60).await?;
|
||||
assert!(claimed);
|
||||
|
||||
let result = try_init_with_roots_and_backfill_lease(
|
||||
home.path().to_path_buf(),
|
||||
home.path().to_path_buf(),
|
||||
"test-provider".to_string(),
|
||||
/*backfill_lease_seconds*/ 60,
|
||||
)
|
||||
.await;
|
||||
let err = match result {
|
||||
Ok(_) => panic!("state db init should not wait forever for incomplete backfill"),
|
||||
Err(err) => err,
|
||||
};
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("timed out waiting for state db backfill"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ async fn insert_state_db_thread(
|
||||
thread_id: ThreadId,
|
||||
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");
|
||||
@@ -87,6 +87,7 @@ async fn insert_state_db_thread(
|
||||
.upsert_thread(&metadata)
|
||||
.await
|
||||
.expect("state db upsert should succeed");
|
||||
runtime
|
||||
}
|
||||
|
||||
// TODO(jif) fix
|
||||
@@ -236,7 +237,7 @@ async fn find_thread_path_falls_back_when_db_path_is_stale() {
|
||||
let stale_db_path = home.join(format!(
|
||||
"sessions/2099/01/01/rollout-2099-01-01T00-00-00-{uuid}.jsonl"
|
||||
));
|
||||
insert_state_db_thread(
|
||||
let runtime = insert_state_db_thread(
|
||||
home,
|
||||
thread_id,
|
||||
stale_db_path.as_path(),
|
||||
@@ -244,7 +245,52 @@ 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())
|
||||
let found = find_thread_path_by_id_str(home, &uuid.to_string(), Some(runtime.as_ref()))
|
||||
.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;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_thread_path_falls_back_when_db_path_points_to_another_thread() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let home = temp.path();
|
||||
let uuid = Uuid::from_u128(304);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let ts = "2025-01-03T13-00-00";
|
||||
write_session_file(
|
||||
home,
|
||||
ts,
|
||||
uuid,
|
||||
/*num_records*/ 1,
|
||||
Some(SessionSource::Cli),
|
||||
)
|
||||
.unwrap();
|
||||
let fs_rollout_path = home.join(format!("sessions/2025/01/03/rollout-{ts}-{uuid}.jsonl"));
|
||||
|
||||
let other_uuid = Uuid::from_u128(1304);
|
||||
let other_ts = "2025-01-04T13-00-00";
|
||||
write_session_file(
|
||||
home,
|
||||
other_ts,
|
||||
other_uuid,
|
||||
/*num_records*/ 1,
|
||||
Some(SessionSource::Cli),
|
||||
)
|
||||
.unwrap();
|
||||
let stale_db_path = home.join(format!(
|
||||
"sessions/2025/01/04/rollout-{other_ts}-{other_uuid}.jsonl"
|
||||
));
|
||||
let runtime = insert_state_db_thread(
|
||||
home,
|
||||
thread_id,
|
||||
stale_db_path.as_path(),
|
||||
/*archived*/ false,
|
||||
)
|
||||
.await;
|
||||
|
||||
let found = find_thread_path_by_id_str(home, &uuid.to_string(), Some(runtime.as_ref()))
|
||||
.await
|
||||
.expect("lookup should succeed");
|
||||
assert_eq!(found, Some(fs_rollout_path.clone()));
|
||||
@@ -269,21 +315,44 @@ 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())
|
||||
let runtime = codex_state::StateRuntime::init(home.to_path_buf(), TEST_PROVIDER.to_string())
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
_runtime
|
||||
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())
|
||||
let found = find_thread_path_by_id_str(home, &uuid.to_string(), Some(runtime.as_ref()))
|
||||
.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;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_thread_path_accepts_existing_state_db_path_without_canonical_filename() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let home = temp.path();
|
||||
let uuid = Uuid::from_u128(305);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let db_rollout_path = home.join("sessions/2025/01/03/custom-rollout-name.jsonl");
|
||||
fs::create_dir_all(db_rollout_path.parent().expect("rollout parent")).unwrap();
|
||||
fs::write(&db_rollout_path, "").unwrap();
|
||||
let runtime = insert_state_db_thread(
|
||||
home,
|
||||
thread_id,
|
||||
db_rollout_path.as_path(),
|
||||
/*archived*/ false,
|
||||
)
|
||||
.await;
|
||||
|
||||
let found = find_thread_path_by_id_str(home, &uuid.to_string(), Some(runtime.as_ref()))
|
||||
.await
|
||||
.expect("lookup should succeed");
|
||||
assert_eq!(found, Some(db_rollout_path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rollout_date_parts_extracts_directory_components() {
|
||||
let file_name = OsStr::new("rollout-2025-03-01T09-00-00-123.jsonl");
|
||||
|
||||
@@ -54,6 +54,7 @@ use codex_core_api::WebSearchMode;
|
||||
use codex_core_api::arg0_dispatch_or_else;
|
||||
use codex_core_api::built_in_model_providers;
|
||||
use codex_core_api::find_codex_home;
|
||||
use codex_core_api::init_state_db;
|
||||
use codex_core_api::item_event_to_server_notification;
|
||||
use codex_core_api::set_default_originator;
|
||||
use codex_core_api::thread_store_from_config;
|
||||
@@ -102,6 +103,7 @@ 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 auth_manager =
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await;
|
||||
@@ -109,7 +111,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);
|
||||
let thread_store = thread_store_from_config(&config, state_db.clone());
|
||||
let environment_manager =
|
||||
Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::new(local_runtime_paths)).await);
|
||||
let thread_manager = ThreadManager::new(
|
||||
@@ -119,6 +121,7 @@ async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
environment_manager,
|
||||
/*analytics_events_client*/ None,
|
||||
Arc::clone(&thread_store),
|
||||
state_db,
|
||||
);
|
||||
|
||||
let NewThread {
|
||||
|
||||
@@ -13,15 +13,19 @@ pub(super) async fn archive_thread(
|
||||
params: ArchiveThreadParams,
|
||||
) -> ThreadStoreResult<()> {
|
||||
let thread_id = params.thread_id;
|
||||
let rollout_path =
|
||||
find_thread_path_by_id_str(store.config.codex_home.as_path(), &thread_id.to_string())
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::InvalidRequest {
|
||||
message: format!("failed to locate thread id {thread_id}: {err}"),
|
||||
})?
|
||||
.ok_or_else(|| ThreadStoreError::InvalidRequest {
|
||||
message: format!("no rollout found for thread id {thread_id}"),
|
||||
})?;
|
||||
let state_db_ctx = store.state_db().await;
|
||||
let rollout_path = find_thread_path_by_id_str(
|
||||
store.config.codex_home.as_path(),
|
||||
&thread_id.to_string(),
|
||||
state_db_ctx.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::InvalidRequest {
|
||||
message: format!("failed to locate thread id {thread_id}: {err}"),
|
||||
})?
|
||||
.ok_or_else(|| ThreadStoreError::InvalidRequest {
|
||||
message: format!("no rollout found for thread id {thread_id}"),
|
||||
})?;
|
||||
|
||||
let canonical_rollout_path = scoped_rollout_path(
|
||||
store.config.codex_home.join(codex_rollout::SESSIONS_SUBDIR),
|
||||
@@ -48,7 +52,7 @@ pub(super) async fn archive_thread(
|
||||
}
|
||||
})?;
|
||||
|
||||
if let Some(ctx) = store.state_db().await {
|
||||
if let Some(ctx) = state_db_ctx {
|
||||
let _ = ctx
|
||||
.mark_archived(thread_id, archived_path.as_path(), Utc::now())
|
||||
.await;
|
||||
@@ -77,7 +81,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
||||
let uuid = Uuid::from_u128(201);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let active_path =
|
||||
@@ -123,7 +127,6 @@ mod tests {
|
||||
async fn archive_thread_updates_sqlite_metadata_when_present() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let store = LocalThreadStore::new(config.clone());
|
||||
let uuid = Uuid::from_u128(202);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let active_path =
|
||||
@@ -134,6 +137,7 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
|
||||
runtime
|
||||
.mark_backfill_complete(/*last_watermark*/ None)
|
||||
.await
|
||||
|
||||
@@ -39,6 +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 rollout_config = RolloutConfig {
|
||||
codex_home: store.config.codex_home.clone(),
|
||||
sqlite_home: store.config.sqlite_home.clone(),
|
||||
@@ -47,6 +48,7 @@ pub(super) async fn list_threads(
|
||||
generate_memories: false,
|
||||
};
|
||||
let page = list_rollout_threads(
|
||||
state_db,
|
||||
&rollout_config,
|
||||
store.config.default_model_provider_id.as_str(),
|
||||
¶ms,
|
||||
@@ -106,6 +108,7 @@ pub(super) async fn list_threads(
|
||||
}
|
||||
|
||||
async fn list_rollout_threads(
|
||||
state_db: Option<codex_rollout::StateDbHandle>,
|
||||
config: &RolloutConfig,
|
||||
default_model_provider_id: &str,
|
||||
params: &ListThreadsParams,
|
||||
@@ -115,6 +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,
|
||||
config,
|
||||
params.page_size,
|
||||
cursor,
|
||||
@@ -129,6 +133,7 @@ async fn list_rollout_threads(
|
||||
.await
|
||||
} else if params.use_state_db_only {
|
||||
RolloutRecorder::list_threads_from_state_db(
|
||||
state_db,
|
||||
config,
|
||||
params.page_size,
|
||||
cursor,
|
||||
@@ -143,6 +148,7 @@ async fn list_rollout_threads(
|
||||
.await
|
||||
} else if params.archived {
|
||||
RolloutRecorder::list_archived_threads(
|
||||
state_db,
|
||||
config,
|
||||
params.page_size,
|
||||
cursor,
|
||||
@@ -157,6 +163,7 @@ async fn list_rollout_threads(
|
||||
.await
|
||||
} else {
|
||||
RolloutRecorder::list_threads(
|
||||
state_db,
|
||||
config,
|
||||
params.page_size,
|
||||
cursor,
|
||||
@@ -196,7 +203,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
||||
write_session_file_with(
|
||||
home.path(),
|
||||
home.path().join("sessions/2025/01/03"),
|
||||
@@ -231,7 +238,6 @@ mod tests {
|
||||
async fn list_threads_preserves_sqlite_title_search_results() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let store = LocalThreadStore::new(config.clone());
|
||||
let uuid = Uuid::from_u128(103);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let rollout_path = home.path().join("rollout-title-search.jsonl");
|
||||
@@ -243,6 +249,7 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
|
||||
runtime
|
||||
.mark_backfill_complete(/*last_watermark*/ None)
|
||||
.await
|
||||
@@ -296,7 +303,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ 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)
|
||||
@@ -365,7 +372,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);
|
||||
let store = LocalThreadStore::new(config, /*state_db*/ None);
|
||||
let uuid = Uuid::from_u128(101);
|
||||
let path =
|
||||
write_session_file(home.path(), "2025-01-03T12-00-00", uuid).expect("session file");
|
||||
@@ -404,7 +411,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
||||
|
||||
let err = store
|
||||
.list_threads(ListThreadsParams {
|
||||
|
||||
@@ -19,7 +19,6 @@ use std::collections::hash_map::Entry;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use crate::AppendThreadItemsParams;
|
||||
use crate::ArchiveThreadParams;
|
||||
@@ -42,7 +41,7 @@ use crate::UpdateThreadMetadataParams;
|
||||
pub struct LocalThreadStore {
|
||||
pub(super) config: LocalThreadStoreConfig,
|
||||
live_recorders: Arc<Mutex<HashMap<ThreadId, RolloutRecorder>>>,
|
||||
state_db: Arc<OnceCell<StateDbHandle>>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
}
|
||||
|
||||
/// Process-scoped configuration for local thread storage.
|
||||
@@ -76,30 +75,18 @@ impl std::fmt::Debug for LocalThreadStore {
|
||||
}
|
||||
|
||||
impl LocalThreadStore {
|
||||
/// Create a local store from process-scoped local storage configuration.
|
||||
pub fn new(config: LocalThreadStoreConfig) -> Self {
|
||||
/// Create a local store using an already initialized state DB handle.
|
||||
pub fn new(config: LocalThreadStoreConfig, state_db: Option<StateDbHandle>) -> Self {
|
||||
Self {
|
||||
config,
|
||||
live_recorders: Arc::new(Mutex::new(HashMap::new())),
|
||||
state_db: Arc::new(OnceCell::new()),
|
||||
state_db,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the state DB handle used by local rollout writers.
|
||||
pub async fn state_db(&self) -> Option<StateDbHandle> {
|
||||
self.state_db
|
||||
.get_or_try_init(|| async {
|
||||
codex_rollout::state_db::init_with_roots(
|
||||
self.config.codex_home.clone(),
|
||||
self.config.sqlite_home.clone(),
|
||||
self.config.default_model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.ok_or(())
|
||||
})
|
||||
.await
|
||||
.ok()
|
||||
.cloned()
|
||||
self.state_db.clone()
|
||||
}
|
||||
|
||||
/// Read a local rollout-backed thread by path.
|
||||
@@ -302,7 +289,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
||||
let thread_id = ThreadId::default();
|
||||
|
||||
store
|
||||
@@ -351,7 +338,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
||||
let thread_id = ThreadId::default();
|
||||
let mut params = create_thread_params(thread_id);
|
||||
params.metadata.cwd = None;
|
||||
@@ -371,7 +358,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
||||
let thread_id = ThreadId::default();
|
||||
|
||||
store
|
||||
@@ -410,7 +397,7 @@ mod tests {
|
||||
let config = test_config(home.path());
|
||||
let thread_id = ThreadId::default();
|
||||
|
||||
let first_store = LocalThreadStore::new(config.clone());
|
||||
let first_store = LocalThreadStore::new(config.clone(), /*state_db*/ None);
|
||||
first_store
|
||||
.create_thread(create_thread_params(thread_id))
|
||||
.await
|
||||
@@ -439,7 +426,7 @@ mod tests {
|
||||
.await
|
||||
.expect("shutdown initial writer");
|
||||
|
||||
let resumed_store = LocalThreadStore::new(config);
|
||||
let resumed_store = LocalThreadStore::new(config, /*state_db*/ None);
|
||||
resumed_store
|
||||
.resume_thread(ResumeThreadParams {
|
||||
thread_id,
|
||||
@@ -470,7 +457,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
||||
let thread_id = ThreadId::default();
|
||||
|
||||
store
|
||||
@@ -490,7 +477,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
||||
let thread_id = ThreadId::default();
|
||||
|
||||
store
|
||||
@@ -519,7 +506,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
||||
let uuid = uuid::Uuid::from_u128(407);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let rollout_path =
|
||||
@@ -548,7 +535,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ 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)
|
||||
@@ -597,7 +584,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ 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)
|
||||
@@ -636,7 +623,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ 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)
|
||||
@@ -704,7 +691,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
||||
let thread_id = ThreadId::default();
|
||||
|
||||
store
|
||||
|
||||
@@ -10,7 +10,6 @@ use codex_rollout::find_thread_name_by_id;
|
||||
use codex_rollout::find_thread_path_by_id_str;
|
||||
use codex_rollout::read_session_meta_line;
|
||||
use codex_rollout::read_thread_item_from_rollout;
|
||||
use codex_state::StateRuntime;
|
||||
use codex_state::ThreadMetadata;
|
||||
|
||||
use super::LocalThreadStore;
|
||||
@@ -172,16 +171,22 @@ async fn resolve_rollout_path(
|
||||
return Ok(Some(path));
|
||||
}
|
||||
|
||||
let state_db_ctx = store.state_db().await;
|
||||
if include_archived {
|
||||
match find_thread_path_by_id_str(store.config.codex_home.as_path(), &thread_id.to_string())
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::InvalidRequest {
|
||||
message: format!("failed to locate thread id {thread_id}: {err}"),
|
||||
})? {
|
||||
match find_thread_path_by_id_str(
|
||||
store.config.codex_home.as_path(),
|
||||
&thread_id.to_string(),
|
||||
state_db_ctx.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::InvalidRequest {
|
||||
message: format!("failed to locate thread id {thread_id}: {err}"),
|
||||
})? {
|
||||
Some(path) => Ok(Some(path)),
|
||||
None => find_archived_thread_path_by_id_str(
|
||||
store.config.codex_home.as_path(),
|
||||
&thread_id.to_string(),
|
||||
state_db_ctx.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::InvalidRequest {
|
||||
@@ -189,11 +194,15 @@ async fn resolve_rollout_path(
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
find_thread_path_by_id_str(store.config.codex_home.as_path(), &thread_id.to_string())
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::InvalidRequest {
|
||||
message: format!("failed to locate thread id {thread_id}: {err}"),
|
||||
})
|
||||
find_thread_path_by_id_str(
|
||||
store.config.codex_home.as_path(),
|
||||
&thread_id.to_string(),
|
||||
state_db_ctx.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::InvalidRequest {
|
||||
message: format!("failed to locate thread id {thread_id}: {err}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,12 +255,7 @@ async fn read_sqlite_metadata(
|
||||
store: &LocalThreadStore,
|
||||
thread_id: codex_protocol::ThreadId,
|
||||
) -> Option<ThreadMetadata> {
|
||||
let runtime = StateRuntime::init(
|
||||
store.config.sqlite_home.clone(),
|
||||
store.config.default_model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.ok()?;
|
||||
let runtime = store.state_db().await?;
|
||||
runtime.get_thread(thread_id).await.ok().flatten()
|
||||
}
|
||||
|
||||
@@ -411,7 +415,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
||||
let uuid = Uuid::from_u128(205);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let active_path =
|
||||
@@ -439,7 +443,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
||||
let uuid = Uuid::from_u128(211);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let active_path =
|
||||
@@ -470,7 +474,6 @@ mod tests {
|
||||
async fn read_thread_by_rollout_path_prefers_sqlite_git_info() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let store = LocalThreadStore::new(config.clone());
|
||||
let uuid = Uuid::from_u128(223);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let active_path =
|
||||
@@ -481,6 +484,7 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
|
||||
let mut builder = ThreadMetadataBuilder::new(
|
||||
thread_id,
|
||||
active_path.clone(),
|
||||
@@ -518,7 +522,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ 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)
|
||||
@@ -559,7 +563,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
||||
let uuid = Uuid::from_u128(208);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let active_path =
|
||||
@@ -584,7 +588,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ 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");
|
||||
@@ -617,7 +621,6 @@ mod tests {
|
||||
async fn read_thread_applies_sqlite_thread_name() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let store = LocalThreadStore::new(config.clone());
|
||||
let uuid = Uuid::from_u128(212);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let rollout_path =
|
||||
@@ -628,6 +631,7 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), 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());
|
||||
@@ -657,7 +661,13 @@ mod tests {
|
||||
async fn read_thread_preserves_rollout_cwd_when_sqlite_metadata_exists() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let store = LocalThreadStore::new(config.clone());
|
||||
let runtime = codex_state::StateRuntime::init(
|
||||
config.sqlite_home.clone(),
|
||||
config.default_model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), 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");
|
||||
@@ -690,12 +700,6 @@ mod tests {
|
||||
});
|
||||
writeln!(file, "{user_event}").expect("write user event");
|
||||
|
||||
let runtime = codex_state::StateRuntime::init(
|
||||
config.sqlite_home.clone(),
|
||||
config.default_model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let mut builder = ThreadMetadataBuilder::new(
|
||||
thread_id,
|
||||
rollout_path.clone(),
|
||||
@@ -732,7 +736,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ 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");
|
||||
@@ -756,7 +760,6 @@ mod tests {
|
||||
async fn read_thread_uses_sqlite_metadata_for_rollout_without_user_preview() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let store = LocalThreadStore::new(config.clone());
|
||||
let uuid = Uuid::from_u128(217);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let day_dir = home.path().join("sessions/2025/01/03");
|
||||
@@ -784,6 +787,7 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
|
||||
let mut builder = ThreadMetadataBuilder::new(
|
||||
thread_id,
|
||||
rollout_path.clone(),
|
||||
@@ -826,7 +830,6 @@ mod tests {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let external = TempDir::new().expect("external temp dir");
|
||||
let config = test_config(home.path());
|
||||
let store = LocalThreadStore::new(config.clone());
|
||||
let uuid = Uuid::from_u128(220);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let rollout_path =
|
||||
@@ -838,6 +841,7 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
|
||||
let mut builder = ThreadMetadataBuilder::new(
|
||||
thread_id,
|
||||
stale_path.clone(),
|
||||
@@ -875,7 +879,6 @@ mod tests {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let external = TempDir::new().expect("external temp dir");
|
||||
let config = test_config(home.path());
|
||||
let store = LocalThreadStore::new(config.clone());
|
||||
let uuid = Uuid::from_u128(221);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let rollout_path =
|
||||
@@ -889,6 +892,7 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), 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());
|
||||
@@ -920,7 +924,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ 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");
|
||||
@@ -975,7 +979,6 @@ mod tests {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let external = TempDir::new().expect("external temp dir");
|
||||
let config = test_config(home.path());
|
||||
let store = LocalThreadStore::new(config.clone());
|
||||
let uuid = Uuid::from_u128(214);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let rollout_path = external
|
||||
@@ -987,6 +990,7 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
|
||||
let mut builder = ThreadMetadataBuilder::new(
|
||||
thread_id,
|
||||
rollout_path.clone(),
|
||||
@@ -1033,7 +1037,6 @@ mod tests {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let external = TempDir::new().expect("external temp dir");
|
||||
let config = test_config(home.path());
|
||||
let store = LocalThreadStore::new(config.clone());
|
||||
let uuid = Uuid::from_u128(216);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let rollout_path = external
|
||||
@@ -1047,6 +1050,7 @@ mod tests {
|
||||
.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()));
|
||||
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());
|
||||
@@ -1089,7 +1093,6 @@ mod tests {
|
||||
async fn read_thread_sqlite_fallback_loads_archived_history() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let store = LocalThreadStore::new(config.clone());
|
||||
let uuid = Uuid::from_u128(219);
|
||||
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)
|
||||
@@ -1100,6 +1103,7 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
|
||||
let mut builder = ThreadMetadataBuilder::new(
|
||||
thread_id,
|
||||
archived_path.clone(),
|
||||
@@ -1135,7 +1139,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
||||
let uuid = Uuid::from_u128(206);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
|
||||
|
||||
@@ -17,9 +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 archived_path = find_archived_thread_path_by_id_str(
|
||||
store.config.codex_home.as_path(),
|
||||
&thread_id.to_string(),
|
||||
state_db_ctx.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::InvalidRequest {
|
||||
@@ -71,7 +73,7 @@ pub(super) async fn unarchive_thread(
|
||||
message: format!("failed to update unarchived thread timestamp: {err}"),
|
||||
})?;
|
||||
|
||||
if let Some(ctx) = store.state_db().await {
|
||||
if let Some(ctx) = state_db_ctx {
|
||||
let _ = ctx
|
||||
.mark_unarchived(thread_id, restored_path.as_path())
|
||||
.await;
|
||||
@@ -116,7 +118,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ 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)
|
||||
@@ -147,7 +149,6 @@ mod tests {
|
||||
async fn unarchive_thread_updates_sqlite_metadata_when_present() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let store = LocalThreadStore::new(config.clone());
|
||||
let uuid = Uuid::from_u128(204);
|
||||
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)
|
||||
@@ -158,6 +159,7 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
|
||||
runtime
|
||||
.mark_backfill_complete(/*last_watermark*/ None)
|
||||
.await
|
||||
|
||||
@@ -157,12 +157,16 @@ async fn resolve_rollout_path(
|
||||
return Ok(ResolvedRolloutPath { path, archived });
|
||||
}
|
||||
|
||||
let active_path =
|
||||
find_thread_path_by_id_str(store.config.codex_home.as_path(), &thread_id.to_string())
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::InvalidRequest {
|
||||
message: format!("failed to locate thread id {thread_id}: {err}"),
|
||||
})?;
|
||||
let state_db_ctx = store.state_db().await;
|
||||
let active_path = find_thread_path_by_id_str(
|
||||
store.config.codex_home.as_path(),
|
||||
&thread_id.to_string(),
|
||||
state_db_ctx.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::InvalidRequest {
|
||||
message: format!("failed to locate thread id {thread_id}: {err}"),
|
||||
})?;
|
||||
if let Some(path) = active_path {
|
||||
return Ok(ResolvedRolloutPath {
|
||||
path,
|
||||
@@ -174,18 +178,22 @@ async fn resolve_rollout_path(
|
||||
message: format!("thread not found: {thread_id}"),
|
||||
});
|
||||
}
|
||||
find_archived_thread_path_by_id_str(store.config.codex_home.as_path(), &thread_id.to_string())
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::InvalidRequest {
|
||||
message: format!("failed to locate archived thread id {thread_id}: {err}"),
|
||||
})?
|
||||
.map(|path| ResolvedRolloutPath {
|
||||
path,
|
||||
archived: true,
|
||||
})
|
||||
.ok_or_else(|| ThreadStoreError::InvalidRequest {
|
||||
message: format!("thread not found: {thread_id}"),
|
||||
})
|
||||
find_archived_thread_path_by_id_str(
|
||||
store.config.codex_home.as_path(),
|
||||
&thread_id.to_string(),
|
||||
state_db_ctx.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::InvalidRequest {
|
||||
message: format!("failed to locate archived thread id {thread_id}: {err}"),
|
||||
})?
|
||||
.map(|path| ResolvedRolloutPath {
|
||||
path,
|
||||
archived: true,
|
||||
})
|
||||
.ok_or_else(|| ThreadStoreError::InvalidRequest {
|
||||
message: format!("thread not found: {thread_id}"),
|
||||
})
|
||||
}
|
||||
|
||||
fn rollout_path_is_archived(store: &LocalThreadStore, path: &std::path::Path) -> bool {
|
||||
@@ -213,7 +221,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
||||
let uuid = Uuid::from_u128(301);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let path =
|
||||
@@ -248,7 +256,6 @@ mod tests {
|
||||
async fn update_thread_metadata_sets_memory_mode_on_active_rollout() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let store = LocalThreadStore::new(config.clone());
|
||||
let uuid = Uuid::from_u128(302);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let path =
|
||||
@@ -259,6 +266,7 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
|
||||
|
||||
let thread = store
|
||||
.update_thread_metadata(UpdateThreadMetadataParams {
|
||||
@@ -288,7 +296,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ 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)
|
||||
@@ -328,7 +336,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ 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");
|
||||
@@ -360,7 +368,7 @@ 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()));
|
||||
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
||||
let uuid = Uuid::from_u128(305);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let path =
|
||||
@@ -395,7 +403,6 @@ mod tests {
|
||||
async fn update_thread_metadata_keeps_archived_thread_archived_in_sqlite() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let store = LocalThreadStore::new(config.clone());
|
||||
let uuid = Uuid::from_u128(306);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let archived_path = write_archived_session_file(home.path(), "2025-01-03T16-00-00", uuid)
|
||||
@@ -406,6 +413,7 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
|
||||
runtime
|
||||
.mark_backfill_complete(/*last_watermark*/ None)
|
||||
.await
|
||||
@@ -458,7 +466,6 @@ mod tests {
|
||||
async fn update_thread_metadata_keeps_live_archived_thread_archived_in_sqlite() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let store = LocalThreadStore::new(config.clone());
|
||||
let uuid = Uuid::from_u128(308);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let archived_path = write_archived_session_file(home.path(), "2025-01-03T16-30-00", uuid)
|
||||
@@ -469,6 +476,7 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
|
||||
runtime
|
||||
.mark_backfill_complete(/*last_watermark*/ None)
|
||||
.await
|
||||
|
||||
@@ -109,6 +109,7 @@ pub use responses_api::mcp_tool_to_deferred_responses_api_tool;
|
||||
pub use responses_api::mcp_tool_to_responses_api_tool;
|
||||
pub use responses_api::tool_definition_to_responses_api_tool;
|
||||
pub use tool_config::ShellCommandBackendConfig;
|
||||
pub use tool_config::ToolEnvironmentMode;
|
||||
pub use tool_config::ToolUserShellType;
|
||||
pub use tool_config::ToolsConfig;
|
||||
pub use tool_config::ToolsConfigParams;
|
||||
|
||||
@@ -88,7 +88,7 @@ pub struct ToolsConfig {
|
||||
pub shell_type: ConfigShellToolType,
|
||||
pub shell_command_backend: ShellCommandBackendConfig,
|
||||
pub unified_exec_shell_mode: UnifiedExecShellMode,
|
||||
pub has_environment: bool,
|
||||
pub environment_mode: ToolEnvironmentMode,
|
||||
pub allow_login_shell: bool,
|
||||
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
|
||||
pub web_search_mode: Option<WebSearchMode>,
|
||||
@@ -129,6 +129,27 @@ pub struct ToolsConfigParams<'a> {
|
||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ToolEnvironmentMode {
|
||||
None,
|
||||
Single,
|
||||
Multiple,
|
||||
}
|
||||
|
||||
impl ToolEnvironmentMode {
|
||||
pub fn from_count(count: usize) -> Self {
|
||||
match count {
|
||||
0 => Self::None,
|
||||
1 => Self::Single,
|
||||
_ => Self::Multiple,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_environment(self) -> bool {
|
||||
!matches!(self, Self::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolsConfig {
|
||||
pub fn new(params: &ToolsConfigParams<'_>) -> Self {
|
||||
let ToolsConfigParams {
|
||||
@@ -205,7 +226,7 @@ impl ToolsConfig {
|
||||
shell_type,
|
||||
shell_command_backend,
|
||||
unified_exec_shell_mode: UnifiedExecShellMode::Direct,
|
||||
has_environment: true,
|
||||
environment_mode: ToolEnvironmentMode::Single,
|
||||
allow_login_shell: true,
|
||||
apply_patch_tool_type,
|
||||
web_search_mode: *web_search_mode,
|
||||
@@ -306,8 +327,8 @@ impl ToolsConfig {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_has_environment(mut self, has_environment: bool) -> Self {
|
||||
self.has_environment = has_environment;
|
||||
pub fn with_environment_mode(mut self, environment_mode: ToolEnvironmentMode) -> Self {
|
||||
self.environment_mode = environment_mode;
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ pub fn build_tool_registry_plan(
|
||||
);
|
||||
}
|
||||
|
||||
if config.has_environment {
|
||||
if config.environment_mode.has_environment() {
|
||||
match &config.shell_type {
|
||||
ConfigShellToolType::Default => {
|
||||
plan.push_spec(
|
||||
@@ -184,7 +184,9 @@ pub fn build_tool_registry_plan(
|
||||
}
|
||||
}
|
||||
|
||||
if config.has_environment && config.shell_type != ConfigShellToolType::Disabled {
|
||||
if config.environment_mode.has_environment()
|
||||
&& config.shell_type != ConfigShellToolType::Disabled
|
||||
{
|
||||
plan.register_handler("shell", ToolHandlerKind::Shell);
|
||||
plan.register_handler("container.exec", ToolHandlerKind::Shell);
|
||||
plan.register_handler("local_shell", ToolHandlerKind::Shell);
|
||||
@@ -324,7 +326,7 @@ pub fn build_tool_registry_plan(
|
||||
);
|
||||
}
|
||||
|
||||
if config.has_environment
|
||||
if config.environment_mode.has_environment()
|
||||
&& let Some(apply_patch_tool_type) = &config.apply_patch_tool_type
|
||||
{
|
||||
match apply_patch_tool_type {
|
||||
@@ -346,7 +348,7 @@ pub fn build_tool_registry_plan(
|
||||
plan.register_handler("apply_patch", ToolHandlerKind::ApplyPatch);
|
||||
}
|
||||
|
||||
if config.has_environment
|
||||
if config.environment_mode.has_environment()
|
||||
&& config
|
||||
.experimental_supported_tools
|
||||
.iter()
|
||||
@@ -393,7 +395,7 @@ pub fn build_tool_registry_plan(
|
||||
);
|
||||
}
|
||||
|
||||
if config.has_environment {
|
||||
if config.environment_mode.has_environment() {
|
||||
plan.push_spec(
|
||||
create_view_image_tool(ViewImageToolOptions {
|
||||
can_request_original_image_detail: config.can_request_original_image_detail,
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::ResponsesApiNamespaceTool;
|
||||
use crate::ResponsesApiTool;
|
||||
use crate::ResponsesApiWebSearchFilters;
|
||||
use crate::ResponsesApiWebSearchUserLocation;
|
||||
use crate::ToolEnvironmentMode;
|
||||
use crate::ToolHandlerSpec;
|
||||
use crate::ToolName;
|
||||
use crate::ToolNamespace;
|
||||
@@ -544,7 +545,7 @@ fn disabled_environment_omits_environment_backed_tools() {
|
||||
permission_profile: &PermissionProfile::Disabled,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
})
|
||||
.with_has_environment(/*has_environment*/ false);
|
||||
.with_environment_mode(ToolEnvironmentMode::None);
|
||||
tools_config
|
||||
.experimental_supported_tools
|
||||
.push("list_dir".to_string());
|
||||
|
||||
@@ -76,6 +76,8 @@ use crate::tui;
|
||||
use crate::tui::TuiEvent;
|
||||
use crate::update_action::UpdateAction;
|
||||
use crate::version::CODEX_CLI_VERSION;
|
||||
use crate::workspace_command::AppServerWorkspaceCommandRunner;
|
||||
use crate::workspace_command::WorkspaceCommandRunner;
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
use codex_app_server_client::AppServerRequestHandle;
|
||||
use codex_app_server_client::TypedRequestError;
|
||||
@@ -144,6 +146,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_terminal_detection::user_agent;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use color_eyre::eyre::Result;
|
||||
@@ -431,8 +434,10 @@ pub(crate) struct App {
|
||||
pub(crate) session_telemetry: SessionTelemetry,
|
||||
pub(crate) app_event_tx: AppEventSender,
|
||||
pub(crate) chat_widget: ChatWidget,
|
||||
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) active_profile: Option<String>,
|
||||
cli_kv_overrides: Vec<(String, TomlValue)>,
|
||||
harness_overrides: ConfigOverrides,
|
||||
@@ -572,6 +577,7 @@ impl App {
|
||||
config: cfg,
|
||||
frame_requester: tui.frame_requester(),
|
||||
app_event_tx: self.app_event_tx.clone(),
|
||||
workspace_command_runner: self.workspace_command_runner.clone(),
|
||||
initial_user_message,
|
||||
enhanced_keys_supported: self.enhanced_keys_supported,
|
||||
has_chatgpt_account: self.chat_widget.has_chatgpt_account(),
|
||||
@@ -609,6 +615,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>,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
) -> Result<AppExitInfo> {
|
||||
use tokio_stream::StreamExt;
|
||||
@@ -709,6 +716,9 @@ impl App {
|
||||
|
||||
let status_line_invalid_items_warned = Arc::new(AtomicBool::new(false));
|
||||
let terminal_title_invalid_items_warned = Arc::new(AtomicBool::new(false));
|
||||
let workspace_command_runner: WorkspaceCommandRunner = Arc::new(
|
||||
AppServerWorkspaceCommandRunner::new(app_server.request_handle()),
|
||||
);
|
||||
let runtime_model_provider_base_url =
|
||||
resolve_runtime_model_provider_base_url(&config.model_provider).await;
|
||||
|
||||
@@ -731,6 +741,7 @@ impl App {
|
||||
config: config.clone(),
|
||||
frame_requester: tui.frame_requester(),
|
||||
app_event_tx: app_event_tx.clone(),
|
||||
workspace_command_runner: Some(workspace_command_runner.clone()),
|
||||
initial_user_message: crate::chatwidget::create_initial_user_message(
|
||||
initial_prompt.clone(),
|
||||
initial_images.clone(),
|
||||
@@ -766,6 +777,7 @@ impl App {
|
||||
config: config.clone(),
|
||||
frame_requester: tui.frame_requester(),
|
||||
app_event_tx: app_event_tx.clone(),
|
||||
workspace_command_runner: Some(workspace_command_runner.clone()),
|
||||
initial_user_message: crate::chatwidget::create_initial_user_message(
|
||||
initial_prompt.clone(),
|
||||
initial_images.clone(),
|
||||
@@ -806,6 +818,7 @@ impl App {
|
||||
config: config.clone(),
|
||||
frame_requester: tui.frame_requester(),
|
||||
app_event_tx: app_event_tx.clone(),
|
||||
workspace_command_runner: Some(workspace_command_runner.clone()),
|
||||
initial_user_message: crate::chatwidget::create_initial_user_message(
|
||||
initial_prompt.clone(),
|
||||
initial_images.clone(),
|
||||
@@ -853,7 +866,9 @@ See the Codex keymap documentation for supported actions and examples."
|
||||
session_telemetry: session_telemetry.clone(),
|
||||
app_event_tx,
|
||||
chat_widget,
|
||||
workspace_command_runner: Some(workspace_command_runner),
|
||||
config,
|
||||
state_db,
|
||||
active_profile,
|
||||
cli_kv_overrides,
|
||||
harness_overrides,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user