Make thread store process-scoped (#19474)

- Build one app-server process ThreadStore from startup config and share
it with ThreadManager and CodexMessageProcessor.
- Remove per-thread/fork store reconstruction so effective thread config
cannot switch the persistence backend.
- Add params to ThreadStore create/resume for specifying thread
metadata, since otherwise the metadata from store creation would be used
(incorrectly).
This commit is contained in:
Tom
2026-04-30 21:24:59 -07:00
committed by GitHub
parent f50c02d7bc
commit fe05acad23
55 changed files with 1076 additions and 514 deletions
@@ -25,6 +25,7 @@ use crate::StoredThread;
use crate::StoredThreadHistory;
use crate::ThreadEventPersistenceMode;
use crate::ThreadMetadataPatch;
use crate::ThreadPersistenceMetadata;
use crate::ThreadSortKey;
use crate::ThreadStoreError;
use crate::ThreadStoreResult;
@@ -186,6 +187,12 @@ pub(super) fn dynamic_tools_json(
serialize_json_vec(dynamic_tools, "dynamic_tool")
}
pub(super) fn thread_persistence_metadata_json(
metadata: &ThreadPersistenceMetadata,
) -> ThreadStoreResult<String> {
serialize_json(metadata, "thread_persistence_metadata")
}
pub(super) fn rollout_items_json(items: &[RolloutItem]) -> ThreadStoreResult<Vec<String>> {
serialize_json_vec(items, "rollout_item")
}
+147
View File
@@ -69,6 +69,7 @@ impl ThreadStore for RemoteThreadStore {
params.event_persistence_mode,
)
.into(),
metadata_json: helpers::thread_persistence_metadata_json(&params.metadata)?,
};
self.client()
.await?
@@ -96,6 +97,7 @@ impl ThreadStore for RemoteThreadStore {
params.event_persistence_mode,
)
.into(),
metadata_json: helpers::thread_persistence_metadata_json(&params.metadata)?,
};
self.client()
.await?
@@ -260,3 +262,148 @@ impl ThreadStore for RemoteThreadStore {
helpers::stored_thread_from_proto(thread)
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use codex_protocol::ThreadId;
use codex_protocol::models::BaseInstructions;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::ThreadMemoryMode;
use pretty_assertions::assert_eq;
use tokio::sync::mpsc;
use tonic::Request;
use tonic::Response;
use tonic::Status;
use tonic::transport::Server;
use super::*;
use crate::ThreadEventPersistenceMode;
use crate::ThreadPersistenceMetadata;
use proto::thread_store_server;
use proto::thread_store_server::ThreadStoreServer;
enum RecordedRequest {
Create(proto::CreateThreadRequest),
Resume(proto::ResumeThreadRequest),
}
struct TestServer {
requests_tx: mpsc::UnboundedSender<RecordedRequest>,
}
#[tonic::async_trait]
impl thread_store_server::ThreadStore for TestServer {
async fn create_thread(
&self,
request: Request<proto::CreateThreadRequest>,
) -> Result<Response<proto::Empty>, Status> {
self.requests_tx
.send(RecordedRequest::Create(request.into_inner()))
.expect("record create request");
Ok(Response::new(proto::Empty {}))
}
async fn resume_thread(
&self,
request: Request<proto::ResumeThreadRequest>,
) -> Result<Response<proto::Empty>, Status> {
self.requests_tx
.send(RecordedRequest::Resume(request.into_inner()))
.expect("record resume request");
Ok(Response::new(proto::Empty {}))
}
async fn list_threads(
&self,
_request: Request<proto::ListThreadsRequest>,
) -> Result<Response<proto::ListThreadsResponse>, Status> {
Err(Status::unimplemented("not implemented"))
}
}
async fn test_store() -> (RemoteThreadStore, mpsc::UnboundedReceiver<RecordedRequest>) {
let (requests_tx, requests_rx) = mpsc::unbounded_channel();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind test server");
let addr = listener.local_addr().expect("test server addr");
tokio::spawn(async move {
Server::builder()
.add_service(ThreadStoreServer::new(TestServer { requests_tx }))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
.await
.expect("test server");
});
(
RemoteThreadStore::new(format!("http://{addr}")),
requests_rx,
)
}
#[tokio::test]
async fn create_thread_forwards_metadata() {
let (store, mut requests_rx) = test_store().await;
let metadata = ThreadPersistenceMetadata {
cwd: Some(PathBuf::from("/workspace")),
model_provider: "test-provider".to_string(),
memory_mode: ThreadMemoryMode::Enabled,
};
store
.create_thread(CreateThreadParams {
thread_id: ThreadId::new(),
forked_from_id: None,
source: SessionSource::Exec,
base_instructions: BaseInstructions::default(),
dynamic_tools: Vec::new(),
metadata: metadata.clone(),
event_persistence_mode: ThreadEventPersistenceMode::Limited,
})
.await
.expect("create thread");
let Some(RecordedRequest::Create(request)) = requests_rx.recv().await else {
panic!("expected create request");
};
assert_eq!(
serde_json::from_str::<ThreadPersistenceMetadata>(&request.metadata_json)
.expect("metadata json"),
metadata
);
}
#[tokio::test]
async fn resume_thread_forwards_metadata() {
let (store, mut requests_rx) = test_store().await;
let metadata = ThreadPersistenceMetadata {
cwd: Some(PathBuf::from("/workspace")),
model_provider: "test-provider".to_string(),
memory_mode: ThreadMemoryMode::Disabled,
};
store
.resume_thread(ResumeThreadParams {
thread_id: ThreadId::new(),
rollout_path: None,
history: None,
include_archived: false,
metadata: metadata.clone(),
event_persistence_mode: ThreadEventPersistenceMode::Limited,
})
.await
.expect("resume thread");
let Some(RecordedRequest::Resume(request)) = requests_rx.recv().await else {
panic!("expected resume request");
};
assert_eq!(
serde_json::from_str::<ThreadPersistenceMetadata>(&request.metadata_json)
.expect("metadata json"),
metadata
);
}
}
@@ -31,6 +31,7 @@ message CreateThreadRequest {
string base_instructions_json = 4;
repeated string dynamic_tools_json = 5;
ThreadEventPersistenceMode event_persistence_mode = 6;
string metadata_json = 7;
}
message ResumeThreadRequest {
@@ -40,6 +41,7 @@ message ResumeThreadRequest {
bool has_history = 4;
bool include_archived = 5;
ThreadEventPersistenceMode event_persistence_mode = 6;
string metadata_json = 7;
}
message AppendThreadItemsRequest {
@@ -22,6 +22,8 @@ pub struct CreateThreadRequest {
pub dynamic_tools_json: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
#[prost(enumeration = "ThreadEventPersistenceMode", tag = "6")]
pub event_persistence_mode: i32,
#[prost(string, tag = "7")]
pub metadata_json: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ResumeThreadRequest {
@@ -37,6 +39,8 @@ pub struct ResumeThreadRequest {
pub include_archived: bool,
#[prost(enumeration = "ThreadEventPersistenceMode", tag = "6")]
pub event_persistence_mode: i32,
#[prost(string, tag = "7")]
pub metadata_json: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct AppendThreadItemsRequest {