[codex] add non-local thread store regression harness (#19266)

- Add an integration test that guarantees nothing gets written to codex
home dir or sqlite when running a rollout with a non-local ThreadStore
- Add an in-memory "spy" ThreadStore for tests like this

Note I could not find a good way to also ensure there were no filesystem
_reads_ that didn't go through threadstore. I explored a more elaborate
sandboxed-subprocess approach but it isn't platform portable and felt
like it wasn't (yet) worth it.
This commit is contained in:
Tom
2026-04-24 15:45:44 -07:00
committed by GitHub
parent 3c6e2638ac
commit 588f7a9fc4
10 changed files with 667 additions and 15 deletions
+5 -4
View File
@@ -1,5 +1,6 @@
use crate::agents_md::DEFAULT_AGENTS_MD_FILENAME;
use crate::agents_md::LOCAL_AGENTS_MD_FILENAME;
use crate::config::ThreadStoreConfig;
use crate::config::edit::ConfigEdit;
use crate::config::edit::ConfigEditsBuilder;
use crate::config::edit::apply_blocking;
@@ -5294,8 +5295,8 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
realtime: RealtimeConfig::default(),
experimental_realtime_ws_backend_prompt: None,
experimental_realtime_ws_startup_context: None,
experimental_thread_store_endpoint: None,
experimental_thread_config_endpoint: None,
experimental_thread_store: ThreadStoreConfig::Local,
base_instructions: None,
developer_instructions: None,
guardian_policy_config: None,
@@ -5492,8 +5493,8 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
realtime: RealtimeConfig::default(),
experimental_realtime_ws_backend_prompt: None,
experimental_realtime_ws_startup_context: None,
experimental_thread_store_endpoint: None,
experimental_thread_config_endpoint: None,
experimental_thread_store: ThreadStoreConfig::Local,
base_instructions: None,
developer_instructions: None,
guardian_policy_config: None,
@@ -5644,8 +5645,8 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
realtime: RealtimeConfig::default(),
experimental_realtime_ws_backend_prompt: None,
experimental_realtime_ws_startup_context: None,
experimental_thread_store_endpoint: None,
experimental_thread_config_endpoint: None,
experimental_thread_store: ThreadStoreConfig::Local,
base_instructions: None,
developer_instructions: None,
guardian_policy_config: None,
@@ -5781,8 +5782,8 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
realtime: RealtimeConfig::default(),
experimental_realtime_ws_backend_prompt: None,
experimental_realtime_ws_startup_context: None,
experimental_thread_store_endpoint: None,
experimental_thread_config_endpoint: None,
experimental_thread_store: ThreadStoreConfig::Local,
base_instructions: None,
developer_instructions: None,
guardian_policy_config: None,
+36 -5
View File
@@ -27,6 +27,7 @@ use codex_config::config_toml::ConfigToml;
use codex_config::config_toml::ProjectConfig;
use codex_config::config_toml::RealtimeAudioConfig;
use codex_config::config_toml::RealtimeConfig;
use codex_config::config_toml::ThreadStoreToml;
use codex_config::config_toml::validate_model_providers;
use codex_config::profile_toml::ConfigProfile;
use codex_config::types::ApprovalsReviewer;
@@ -230,6 +231,19 @@ impl Permissions {
}
}
/// Configured thread persistence backend.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum ThreadStoreConfig {
/// Persist threads locally using rollout JSONL files and sqlite metadata.
#[default]
Local,
/// Persist threads through the remote thread-store service.
Remote { endpoint: String },
/// Test-only in-memory thread store.
#[cfg(debug_assertions)]
InMemory { id: String },
}
/// Application configuration loaded from disk and merged with overrides.
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
@@ -545,13 +559,12 @@ pub struct Config {
/// active.
pub experimental_realtime_start_instructions: Option<String>,
/// Experimental / do not use. When set, app-server uses a remote thread
/// store at this endpoint instead of the local filesystem/SQLite store.
pub experimental_thread_store_endpoint: Option<String>,
/// Experimental / do not use. When set, app-server fetches thread-scoped
/// config from a remote service at this endpoint.
pub experimental_thread_config_endpoint: Option<String>,
/// Experimental / do not use. Selects the thread persistence backend.
pub experimental_thread_store: ThreadStoreConfig,
/// When set, restricts ChatGPT login to a specific workspace identifier.
pub forced_chatgpt_workspace_id: Option<String>,
@@ -1297,6 +1310,21 @@ fn resolve_tool_suggest_config(config_toml: &ConfigToml) -> ToolSuggestConfig {
ToolSuggestConfig { discoverables }
}
fn thread_store_config(
thread_store: Option<ThreadStoreToml>,
legacy_remote_endpoint: Option<String>,
) -> ThreadStoreConfig {
match thread_store {
Some(ThreadStoreToml::Local {}) => ThreadStoreConfig::Local,
Some(ThreadStoreToml::Remote { endpoint }) => ThreadStoreConfig::Remote { endpoint },
#[cfg(debug_assertions)]
Some(ThreadStoreToml::InMemory { id }) => ThreadStoreConfig::InMemory { id },
None => legacy_remote_endpoint.map_or(ThreadStoreConfig::Local, |endpoint| {
ThreadStoreConfig::Remote { endpoint }
}),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PermissionConfigSyntax {
Legacy,
@@ -2434,8 +2462,11 @@ impl Config {
experimental_realtime_ws_backend_prompt: cfg.experimental_realtime_ws_backend_prompt,
experimental_realtime_ws_startup_context: cfg.experimental_realtime_ws_startup_context,
experimental_realtime_start_instructions: cfg.experimental_realtime_start_instructions,
experimental_thread_store_endpoint: cfg.experimental_thread_store_endpoint,
experimental_thread_config_endpoint: cfg.experimental_thread_config_endpoint,
experimental_thread_store: thread_store_config(
cfg.experimental_thread_store,
cfg.experimental_thread_store_endpoint,
),
forced_chatgpt_workspace_id,
forced_login_method,
include_apply_patch_tool: include_apply_patch_tool_flag,
+10 -3
View File
@@ -2,6 +2,7 @@ use crate::SkillsManager;
use crate::agent::AgentControl;
use crate::codex_thread::CodexThread;
use crate::config::Config;
use crate::config::ThreadStoreConfig;
use crate::environment_selection::default_thread_environment_selections;
use crate::environment_selection::selected_primary_environment;
use crate::environment_selection::validate_environment_selections;
@@ -52,6 +53,8 @@ use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::W3cTraceContext;
use codex_rollout::RolloutConfig;
use codex_state::DirectionalThreadSpawnEdgeStatus;
#[cfg(debug_assertions)]
use codex_thread_store::InMemoryThreadStore;
use codex_thread_store::LocalThreadStore;
use codex_thread_store::RemoteThreadStore;
use codex_thread_store::ThreadStore;
@@ -251,10 +254,14 @@ pub fn build_models_manager(
}
fn configured_thread_store(config: &Config) -> Arc<dyn ThreadStore> {
if let Some(endpoint) = config.experimental_thread_store_endpoint.as_deref() {
return Arc::new(RemoteThreadStore::new(endpoint));
match &config.experimental_thread_store {
ThreadStoreConfig::Local => {
Arc::new(LocalThreadStore::new(RolloutConfig::from_view(config)))
}
ThreadStoreConfig::Remote { endpoint } => Arc::new(RemoteThreadStore::new(endpoint)),
#[cfg(debug_assertions)]
ThreadStoreConfig::InMemory { id } => InMemoryThreadStore::for_id(id),
}
Arc::new(LocalThreadStore::new(RolloutConfig::from_view(config)))
}
impl ThreadManager {