Skip to content

Commit ec3b1ca

Browse files
committed
[app-server] allow custom in-process thread stores
1 parent f2f80ef commit ec3b1ca

4 files changed

Lines changed: 105 additions & 9 deletions

File tree

codex-rs/app-server/src/in_process.rs

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ use codex_login::AuthManager;
8888
use codex_protocol::protocol::SessionSource;
8989
pub use codex_rollout::StateDbHandle;
9090
pub use codex_state::log_db::LogDbLayer;
91+
use codex_thread_store::ThreadStore;
9192
use tokio::sync::mpsc;
9293
use tokio::sync::oneshot;
9394
use tokio::time::timeout;
@@ -150,6 +151,23 @@ pub struct InProcessStartArgs {
150151
pub channel_capacity: usize,
151152
}
152153

154+
/// Optional dependencies and behavior overrides for an in-process runtime.
155+
///
156+
/// Use [`InProcessStartOptions::default`] to preserve the standard app-server
157+
/// startup behavior.
158+
#[derive(Default)]
159+
pub struct InProcessStartOptions {
160+
thread_store: Option<Arc<dyn ThreadStore>>,
161+
}
162+
163+
impl InProcessStartOptions {
164+
/// Uses `thread_store` instead of the store derived from the runtime config.
165+
pub fn with_thread_store(mut self, thread_store: Arc<dyn ThreadStore>) -> Self {
166+
self.thread_store = Some(thread_store);
167+
self
168+
}
169+
}
170+
153171
/// Event emitted from the app-server to the in-process client.
154172
///
155173
/// [`Lagged`](Self::Lagged) is a transport health marker, not an application
@@ -350,8 +368,16 @@ impl InProcessClientHandle {
350368
/// the handle, so callers receive a ready-to-use runtime. If initialize fails,
351369
/// the runtime is shut down and an `InvalidData` error is returned.
352370
pub async fn start(args: InProcessStartArgs) -> IoResult<InProcessClientHandle> {
371+
start_with_options(args, InProcessStartOptions::default()).await
372+
}
373+
374+
/// Starts an in-process app-server runtime with explicit dependency overrides.
375+
pub async fn start_with_options(
376+
args: InProcessStartArgs,
377+
options: InProcessStartOptions,
378+
) -> IoResult<InProcessClientHandle> {
353379
let initialize = args.initialize.clone();
354-
let client = start_uninitialized(args).await?;
380+
let client = start_uninitialized(args, options).await?;
355381

356382
let initialize_response = client
357383
.request(ClientRequest::Initialize {
@@ -371,7 +397,10 @@ pub async fn start(args: InProcessStartArgs) -> IoResult<InProcessClientHandle>
371397
Ok(client)
372398
}
373399

374-
async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClientHandle> {
400+
async fn start_uninitialized(
401+
args: InProcessStartArgs,
402+
options: InProcessStartOptions,
403+
) -> IoResult<InProcessClientHandle> {
375404
let channel_capacity = args.channel_capacity.max(1);
376405
let installation_id = resolve_installation_id(&args.config.codex_home).await?;
377406
let (client_tx, mut client_rx) = mpsc::channel::<InProcessClientMessage>(channel_capacity);
@@ -440,6 +469,7 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
440469
rpc_transport: AppServerRpcTransport::InProcess,
441470
remote_control_handle: None,
442471
plugin_startup_tasks: crate::PluginStartupTasks::Start,
472+
thread_store: options.thread_store,
443473
}));
444474
let mut thread_created_rx = processor.thread_created_receiver();
445475
let session = Arc::new(ConnectionSessionState::new());
@@ -733,13 +763,15 @@ mod tests {
733763
use codex_app_server_protocol::ConfigRequirementsReadResponse;
734764
use codex_app_server_protocol::ExternalAgentConfigImportCompletedNotification;
735765
use codex_app_server_protocol::SessionSource as ApiSessionSource;
766+
use codex_app_server_protocol::ThreadListParams;
736767
use codex_app_server_protocol::ThreadStartParams;
737768
use codex_app_server_protocol::ThreadStartResponse;
738769
use codex_app_server_protocol::Turn;
739770
use codex_app_server_protocol::TurnCompletedNotification;
740771
use codex_app_server_protocol::TurnItemsView;
741772
use codex_app_server_protocol::TurnStatus;
742773
use codex_core::config::ConfigBuilder;
774+
use codex_thread_store::InMemoryThreadStore;
743775
use pretty_assertions::assert_eq;
744776
use std::path::Path;
745777
use tempfile::TempDir;
@@ -760,15 +792,12 @@ mod tests {
760792
}
761793
}
762794

763-
async fn start_test_client_with_capacity(
795+
async fn build_test_start_args(
764796
session_source: SessionSource,
765797
channel_capacity: usize,
766-
) -> InProcessClientHandle {
798+
) -> (TempDir, InProcessStartArgs) {
767799
let codex_home = TempDir::new().expect("temp dir");
768800
let config = Arc::new(build_test_config(codex_home.path()).await);
769-
let state_db = codex_rollout::state_db::try_init(config.as_ref())
770-
.await
771-
.expect("state db should initialize for in-process test");
772801
let args = InProcessStartArgs {
773802
arg0_paths: Arg0DispatchPaths::default(),
774803
config,
@@ -779,7 +808,7 @@ mod tests {
779808
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
780809
feedback: CodexFeedback::new(),
781810
log_db: None,
782-
state_db: Some(state_db),
811+
state_db: None,
783812
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
784813
config_warnings: Vec::new(),
785814
session_source,
@@ -794,6 +823,27 @@ mod tests {
794823
},
795824
channel_capacity,
796825
};
826+
(codex_home, args)
827+
}
828+
829+
async fn start_test_client_with_capacity_and_options(
830+
session_source: SessionSource,
831+
channel_capacity: usize,
832+
options: InProcessStartOptions,
833+
) -> InProcessClientHandle {
834+
let (codex_home, args) = build_test_start_args(session_source, channel_capacity).await;
835+
let mut client = start_with_options(args, options)
836+
.await
837+
.expect("in-process runtime should start");
838+
client._test_codex_home = Some(codex_home);
839+
client
840+
}
841+
842+
async fn start_test_client_with_capacity(
843+
session_source: SessionSource,
844+
channel_capacity: usize,
845+
) -> InProcessClientHandle {
846+
let (codex_home, args) = build_test_start_args(session_source, channel_capacity).await;
797847
let mut client = start(args).await.expect("in-process runtime should start");
798848
client._test_codex_home = Some(codex_home);
799849
client
@@ -852,6 +902,45 @@ mod tests {
852902
}
853903
}
854904

905+
#[tokio::test]
906+
async fn in_process_start_uses_injected_thread_store() {
907+
let thread_store = Arc::new(InMemoryThreadStore::default());
908+
let client = start_test_client_with_capacity_and_options(
909+
SessionSource::Cli,
910+
DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
911+
InProcessStartOptions::default().with_thread_store(thread_store.clone()),
912+
)
913+
.await;
914+
915+
client
916+
.request(ClientRequest::ThreadList {
917+
request_id: RequestId::Integer(3),
918+
params: ThreadListParams {
919+
cursor: None,
920+
limit: Some(1),
921+
sort_key: None,
922+
sort_direction: None,
923+
model_providers: Some(Vec::new()),
924+
source_kinds: None,
925+
archived: None,
926+
cwd: None,
927+
use_state_db_only: false,
928+
search_term: None,
929+
parent_thread_id: None,
930+
ancestor_thread_id: None,
931+
},
932+
})
933+
.await
934+
.expect("request transport should work")
935+
.expect("thread/list should succeed");
936+
937+
assert_eq!(thread_store.calls().await.list_threads, 1);
938+
client
939+
.shutdown()
940+
.await
941+
.expect("in-process runtime should shutdown cleanly");
942+
}
943+
855944
#[tokio::test]
856945
async fn in_process_start_clamps_zero_channel_capacity() {
857946
let client =

codex-rs/app-server/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,7 @@ pub async fn run_main_with_transport_options(
901901
rpc_transport: analytics_rpc_transport(&transport),
902902
remote_control_handle: Some(remote_control_handle.clone()),
903903
plugin_startup_tasks: runtime_options.plugin_startup_tasks,
904+
thread_store: None,
904905
}));
905906
let mut thread_created_rx = processor.thread_created_receiver();
906907
let mut running_turn_count_rx = processor.subscribe_running_assistant_turn_count();

codex-rs/app-server/src/message_processor.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ use codex_protocol::protocol::SessionSource;
8484
use codex_protocol::protocol::W3cTraceContext;
8585
use codex_rollout::StateDbHandle;
8686
use codex_state::log_db::LogDbLayer;
87+
use codex_thread_store::ThreadStore;
8788
use tokio::sync::Mutex;
8889
use tokio::sync::Semaphore;
8990
use tokio::sync::broadcast;
@@ -303,6 +304,7 @@ pub(crate) struct MessageProcessorArgs {
303304
pub(crate) rpc_transport: AppServerRpcTransport,
304305
pub(crate) remote_control_handle: Option<RemoteControlHandle>,
305306
pub(crate) plugin_startup_tasks: crate::PluginStartupTasks,
307+
pub(crate) thread_store: Option<Arc<dyn ThreadStore>>,
306308
}
307309

308310
impl MessageProcessor {
@@ -326,6 +328,7 @@ impl MessageProcessor {
326328
rpc_transport,
327329
remote_control_handle,
328330
plugin_startup_tasks,
331+
thread_store,
329332
} = args;
330333
auth_manager.set_external_auth(Arc::new(ExternalAuthRefreshBridge {
331334
outgoing: outgoing.clone(),
@@ -334,7 +337,9 @@ impl MessageProcessor {
334337
// The thread store is intentionally process-scoped. Config reloads can
335338
// affect per-thread behavior, but they must not move newly started,
336339
// resumed, or forked threads to a different persistence backend/root.
337-
let thread_store = codex_core::thread_store_from_config(config.as_ref(), state_db.clone());
340+
let thread_store = thread_store.unwrap_or_else(|| {
341+
codex_core::thread_store_from_config(config.as_ref(), state_db.clone())
342+
});
338343
let environment_manager_for_requests = Arc::clone(&environment_manager);
339344
let environment_manager_for_extensions = Arc::clone(&environment_manager);
340345
let restriction_product = session_source.restriction_product();

codex-rs/app-server/src/message_processor_tracing_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ async fn build_test_processor(
267267
rpc_transport: AppServerRpcTransport::Stdio,
268268
remote_control_handle: None,
269269
plugin_startup_tasks: crate::PluginStartupTasks::Start,
270+
thread_store: None,
270271
}));
271272
(processor, outgoing_rx)
272273
}

0 commit comments

Comments
 (0)