mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Remove remote thread store implementation (#21596)
Remove the remote thread-store backend and checked-in protobuf artifacts. We've moved these into another crate that link against this one. Also remove the config settings for thread store backend selection, since we'll instead pass an instantiated thread store into the core-api crate's main entrypoint.
This commit is contained in:
Generated
-123
@@ -1180,12 +1180,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base16ct"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
@@ -3609,16 +3603,11 @@ dependencies = [
|
||||
"codex-rollout",
|
||||
"codex-state",
|
||||
"pretty_assertions",
|
||||
"prost 0.14.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tonic",
|
||||
"tonic-prost",
|
||||
"tonic-prost-build",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
@@ -4425,18 +4414,6 @@ version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
@@ -5150,20 +5127,6 @@ version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.16.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
||||
dependencies = [
|
||||
"der",
|
||||
"digest",
|
||||
"elliptic-curve",
|
||||
"rfc6979",
|
||||
"signature",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
@@ -5197,26 +5160,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.13.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"crypto-bigint",
|
||||
"digest",
|
||||
"ff",
|
||||
"generic-array",
|
||||
"group",
|
||||
"pem-rfc7468",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"sec1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ena"
|
||||
version = "0.14.3"
|
||||
@@ -5470,16 +5413,6 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
@@ -6874,17 +6807,6 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||
dependencies = [
|
||||
"ff",
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gzip-header"
|
||||
version = "1.0.0"
|
||||
@@ -9384,18 +9306,6 @@ dependencies = [
|
||||
"supports-color 3.0.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
|
||||
dependencies = [
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
"primeorder",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
@@ -9825,15 +9735,6 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "primeorder"
|
||||
version = "0.13.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
|
||||
dependencies = [
|
||||
"elliptic-curve",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.4.0"
|
||||
@@ -10784,16 +10685,6 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"
|
||||
|
||||
[[package]]
|
||||
name = "rfc6979"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -11253,20 +11144,6 @@ version = "3.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"der",
|
||||
"generic-array",
|
||||
"pkcs8",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "seccompiler"
|
||||
version = "0.5.0"
|
||||
|
||||
@@ -12,7 +12,6 @@ use codex_app_server_protocol::GetConversationSummaryParams;
|
||||
use codex_app_server_protocol::GetConversationSummaryResponse;
|
||||
use codex_app_server_protocol::InitializeCapabilities;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
@@ -46,7 +45,6 @@ const CREATED_AT_RFC3339: &str = "2025-01-02T12:00:00.000Z";
|
||||
const UPDATED_AT_RFC3339: &str = "2025-01-02T12:00:00.000Z";
|
||||
const PREVIEW: &str = "Summarize this conversation";
|
||||
const MODEL_PROVIDER: &str = "openai";
|
||||
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
|
||||
|
||||
fn expected_summary(conversation_id: ThreadId, path: PathBuf) -> ConversationSummary {
|
||||
ConversationSummary {
|
||||
@@ -114,37 +112,6 @@ async fn get_conversation_summary_by_thread_id_reads_rollout() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn get_conversation_summary_by_rollout_path_rejects_remote_thread_store() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
r#"experimental_thread_store_endpoint = "http://127.0.0.1:1"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_get_conversation_summary_request(GetConversationSummaryParams::RolloutPath {
|
||||
rollout_path: PathBuf::from("sessions/2025/01/02/rollout.jsonl"),
|
||||
})
|
||||
.await?;
|
||||
let error: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE);
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
"rollout path queries are only supported with the local thread store"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
//!
|
||||
//! The app-server startup path should honor `experimental_thread_store`
|
||||
//! by routing all thread persistence through the configured store. This suite uses
|
||||
//! the thread-store crate's test-only in-memory store, which exercises the same
|
||||
//! config-driven selection path as a remote store without requiring the real gRPC
|
||||
//! service.
|
||||
//! the thread-store crate's test-only in-memory store to exercise the non-local
|
||||
//! config-driven selection path without touching local rollout or sqlite storage.
|
||||
//!
|
||||
//! The important failure mode is accidentally materializing local persistence
|
||||
//! while a non-local store is configured. After `thread/start` and a simple turn,
|
||||
|
||||
@@ -33,7 +33,6 @@ use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
use wiremock::Mock;
|
||||
@@ -51,7 +50,6 @@ use super::analytics::wait_for_analytics_payload;
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(25);
|
||||
#[cfg(not(windows))]
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
const INTERNAL_ERROR_CODE: i64 = -32603;
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> {
|
||||
@@ -269,37 +267,6 @@ async fn thread_fork_can_load_source_by_path() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_fork_by_path_uses_remote_thread_store_error() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_with_remote_thread_store(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let fork_id = mcp
|
||||
.send_thread_fork_request(ThreadForkParams {
|
||||
thread_id: "not-a-valid-thread-id".to_string(),
|
||||
path: Some(PathBuf::from("sessions/2025/01/05/rollout.jsonl")),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let fork_err: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(fork_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(fork_err.error.code, INTERNAL_ERROR_CODE);
|
||||
assert_eq!(
|
||||
fork_err.error.message,
|
||||
"failed to read thread: thread-store internal error: remote thread store does not support read_thread_by_rollout_path"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_fork_emits_restored_token_usage_before_next_turn() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
@@ -779,33 +746,6 @@ stream_max_retries = 0
|
||||
)
|
||||
}
|
||||
|
||||
fn create_config_toml_with_remote_thread_store(
|
||||
codex_home: &Path,
|
||||
server_uri: &str,
|
||||
) -> std::io::Result<()> {
|
||||
let config_toml = codex_home.join("config.toml");
|
||||
std::fs::write(
|
||||
config_toml,
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "read-only"
|
||||
experimental_thread_store_endpoint = "http://127.0.0.1:1"
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "{server_uri}/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn create_config_toml_with_chatgpt_base_url(
|
||||
codex_home: &Path,
|
||||
server_uri: &str,
|
||||
|
||||
@@ -95,7 +95,6 @@ use super::analytics::wait_for_analytics_payload;
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(25);
|
||||
#[cfg(not(windows))]
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
const INTERNAL_ERROR_CODE: i64 = -32603;
|
||||
const CODEX_5_2_INSTRUCTIONS_TEMPLATE_DEFAULT: &str = "You are Codex, a coding agent based on GPT-5. You and the user share the same workspace and collaborate to achieve the user's goals.";
|
||||
|
||||
fn normalized_existing_path(path: impl AsRef<Path>) -> Result<PathBuf> {
|
||||
@@ -745,37 +744,6 @@ async fn thread_goal_clear_deletes_goal_and_notifies() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_resume_by_path_uses_remote_thread_store_error() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_with_remote_thread_store(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let resume_id = mcp
|
||||
.send_thread_resume_request(ThreadResumeParams {
|
||||
thread_id: "ignored-when-path-is-present".to_string(),
|
||||
path: Some(PathBuf::from("sessions/2025/01/05/rollout.jsonl")),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let resume_err: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(resume_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(resume_err.error.code, INTERNAL_ERROR_CODE);
|
||||
assert_eq!(
|
||||
resume_err.error.message,
|
||||
"failed to read thread: thread-store internal error: remote thread store does not support read_thread_by_rollout_path"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_resume_emits_restored_token_usage_before_next_turn() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
@@ -2928,36 +2896,6 @@ stream_max_retries = 0
|
||||
)
|
||||
}
|
||||
|
||||
fn create_config_toml_with_remote_thread_store(
|
||||
codex_home: &std::path::Path,
|
||||
server_uri: &str,
|
||||
) -> std::io::Result<()> {
|
||||
let config_toml = codex_home.join("config.toml");
|
||||
std::fs::write(
|
||||
config_toml,
|
||||
format!(
|
||||
r#"
|
||||
model = "gpt-5.3-codex"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "read-only"
|
||||
experimental_thread_store_endpoint = "http://127.0.0.1:1"
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[features]
|
||||
personality = true
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "{server_uri}/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn create_config_toml_with_chatgpt_base_url(
|
||||
codex_home: &std::path::Path,
|
||||
server_uri: &str,
|
||||
|
||||
@@ -345,14 +345,15 @@ pub struct ConfigToml {
|
||||
/// 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>,
|
||||
|
||||
/// Removed. Former remote thread-store endpoint setting kept only so we can
|
||||
/// fail fast instead of silently falling back to local persistence.
|
||||
#[schemars(skip)]
|
||||
pub experimental_thread_store_endpoint: Option<String>,
|
||||
|
||||
/// Experimental / do not use. Selects the thread store implementation.
|
||||
pub experimental_thread_store: Option<ThreadStoreToml>,
|
||||
pub projects: Option<HashMap<String, ProjectConfig>>,
|
||||
@@ -488,9 +489,6 @@ pub struct DebugConfigLockToml {
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ThreadStoreToml {
|
||||
Local {},
|
||||
Remote {
|
||||
endpoint: String,
|
||||
},
|
||||
#[schemars(skip)]
|
||||
InMemory {
|
||||
id: String,
|
||||
|
||||
@@ -2355,24 +2355,6 @@
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"endpoint": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"remote"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"endpoint",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3932,10 +3914,6 @@
|
||||
],
|
||||
"description": "Experimental / do not use. Selects the thread store implementation."
|
||||
},
|
||||
"experimental_thread_store_endpoint": {
|
||||
"description": "Experimental / do not use. When set, app-server uses a remote thread store at this endpoint instead of the local filesystem/SQLite store.",
|
||||
"type": "string"
|
||||
},
|
||||
"experimental_use_freeform_apply_patch": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -717,11 +717,13 @@ impl AgentControl {
|
||||
let result = if let Ok(thread) = state.get_thread(agent_id).await {
|
||||
thread.codex.session.ensure_rollout_materialized().await;
|
||||
thread.codex.session.flush_rollout().await?;
|
||||
if matches!(thread.agent_status().await, AgentStatus::Shutdown) {
|
||||
let result = if matches!(thread.agent_status().await, AgentStatus::Shutdown) {
|
||||
Ok(String::new())
|
||||
} else {
|
||||
state.send_op(agent_id, Op::Shutdown {}).await
|
||||
}
|
||||
};
|
||||
thread.wait_until_terminated().await;
|
||||
result
|
||||
} else {
|
||||
state.send_op(agent_id, Op::Shutdown {}).await
|
||||
};
|
||||
|
||||
@@ -2278,6 +2278,27 @@ async fn runtime_config_resolves_terminal_resize_reflow_defaults_and_overrides()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_remote_thread_store_endpoint_is_rejected() {
|
||||
let cfg: ConfigToml =
|
||||
toml::from_str(r#"experimental_thread_store_endpoint = "https://example.com""#)
|
||||
.expect("legacy remote thread-store endpoint should still deserialize");
|
||||
|
||||
let err = Config::load_from_base_config_with_overrides(
|
||||
cfg,
|
||||
ConfigOverrides::default(),
|
||||
tempdir().expect("tempdir").abs(),
|
||||
)
|
||||
.await
|
||||
.expect_err("legacy remote thread-store endpoint should be rejected at load time");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("experimental_thread_store_endpoint")
|
||||
);
|
||||
assert!(err.to_string().contains("no longer supported"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_tui_rejects_unsupported_settings() {
|
||||
let err = toml::from_str::<ConfigToml>(
|
||||
|
||||
@@ -380,8 +380,6 @@ 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 },
|
||||
/// In-memory thread store for test and debug configurations.
|
||||
InMemory { id: String },
|
||||
}
|
||||
@@ -1733,17 +1731,11 @@ fn resolve_tool_suggest_config_from_config(
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_store_config(
|
||||
thread_store: Option<ThreadStoreToml>,
|
||||
legacy_remote_endpoint: Option<String>,
|
||||
) -> ThreadStoreConfig {
|
||||
fn thread_store_config(thread_store: Option<ThreadStoreToml>) -> ThreadStoreConfig {
|
||||
match thread_store {
|
||||
Some(ThreadStoreToml::Local {}) => ThreadStoreConfig::Local,
|
||||
Some(ThreadStoreToml::Remote { endpoint }) => ThreadStoreConfig::Remote { endpoint },
|
||||
Some(ThreadStoreToml::InMemory { id }) => ThreadStoreConfig::InMemory { id },
|
||||
None => legacy_remote_endpoint.map_or(ThreadStoreConfig::Local, |endpoint| {
|
||||
ThreadStoreConfig::Remote { endpoint }
|
||||
}),
|
||||
None => ThreadStoreConfig::Local,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2097,6 +2089,13 @@ impl Config {
|
||||
) -> std::io::Result<Self> {
|
||||
// Keep the large config-construction future off small test thread stacks.
|
||||
Box::pin(async move {
|
||||
if cfg.experimental_thread_store_endpoint.is_some() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"`experimental_thread_store_endpoint` is no longer supported; remove it from config.toml",
|
||||
));
|
||||
}
|
||||
|
||||
validate_model_providers(&cfg.model_providers)
|
||||
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
|
||||
// Ensure that every field of ConfigRequirements is applied to the final
|
||||
@@ -3122,10 +3121,7 @@ impl Config {
|
||||
experimental_realtime_ws_startup_context: cfg.experimental_realtime_ws_startup_context,
|
||||
experimental_realtime_start_instructions: cfg.experimental_realtime_start_instructions,
|
||||
experimental_thread_config_endpoint: cfg.experimental_thread_config_endpoint,
|
||||
experimental_thread_store: thread_store_config(
|
||||
cfg.experimental_thread_store,
|
||||
cfg.experimental_thread_store_endpoint,
|
||||
),
|
||||
experimental_thread_store: thread_store_config(cfg.experimental_thread_store),
|
||||
forced_chatgpt_workspace_id,
|
||||
forced_login_method,
|
||||
include_apply_patch_tool: include_apply_patch_tool_flag,
|
||||
|
||||
@@ -57,7 +57,6 @@ use codex_thread_store::LocalThreadStore;
|
||||
use codex_thread_store::LocalThreadStoreConfig;
|
||||
use codex_thread_store::ReadThreadByRolloutPathParams;
|
||||
use codex_thread_store::ReadThreadParams;
|
||||
use codex_thread_store::RemoteThreadStore;
|
||||
use codex_thread_store::StoredThread;
|
||||
use codex_thread_store::ThreadStore;
|
||||
use codex_thread_store::ThreadStoreError;
|
||||
@@ -277,7 +276,6 @@ pub fn thread_store_from_config(
|
||||
LocalThreadStoreConfig::from_config(config),
|
||||
state_db,
|
||||
)),
|
||||
ThreadStoreConfig::Remote { endpoint } => Arc::new(RemoteThreadStore::new(endpoint)),
|
||||
ThreadStoreConfig::InMemory { id } => InMemoryThreadStore::for_id(id),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,6 @@ name = "codex_thread_store"
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[[example]]
|
||||
name = "generate-proto"
|
||||
path = "examples/generate-proto.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -23,20 +19,14 @@ codex-git-utils = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-rollout = { workspace = true }
|
||||
codex-state = { workspace = true }
|
||||
prost = "0.14.3"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tonic = { workspace = true }
|
||||
tonic-prost = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
tokio-stream = { workspace = true, features = ["net"] }
|
||||
tonic = { workspace = true, features = ["router", "transport"] }
|
||||
tonic-prost-build = { version = "=0.14.3", default-features = false, features = ["transport"] }
|
||||
uuid = { workspace = true }
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let Some(proto_dir_arg) = std::env::args().nth(1) else {
|
||||
eprintln!("Usage: generate-proto <proto-dir>");
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let proto_dir = PathBuf::from(proto_dir_arg);
|
||||
let proto_file = proto_dir.join("codex.thread_store.v1.proto");
|
||||
|
||||
tonic_prost_build::configure()
|
||||
.build_client(true)
|
||||
.build_server(true)
|
||||
.out_dir(&proto_dir)
|
||||
.compile_protos(&[proto_file], &[proto_dir])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd "$script_dir/../../.." && pwd)"
|
||||
proto_dir="$repo_root/codex-rs/thread-store/src/remote/proto"
|
||||
generated="$proto_dir/codex.thread_store.v1.rs"
|
||||
tmpdir="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
(
|
||||
cd "$repo_root/codex-rs"
|
||||
CARGO_TARGET_DIR="$tmpdir/target" cargo run \
|
||||
-p codex-thread-store \
|
||||
--example generate-proto \
|
||||
-- "$proto_dir"
|
||||
)
|
||||
|
||||
if ! sed -n '2p' "$generated" | grep -q 'clippy::trivially_copy_pass_by_ref'; then
|
||||
{
|
||||
sed -n '1p' "$generated"
|
||||
printf '#![allow(clippy::trivially_copy_pass_by_ref)]\n'
|
||||
sed '1d' "$generated"
|
||||
} > "$tmpdir/generated.rs"
|
||||
mv "$tmpdir/generated.rs" "$generated"
|
||||
fi
|
||||
|
||||
rustfmt --edition 2024 "$generated"
|
||||
|
||||
awk '
|
||||
NR == 3 && previous ~ /clippy::trivially_copy_pass_by_ref/ && $0 != "" { print "" }
|
||||
{ print; previous = $0 }
|
||||
' "$generated" > "$tmpdir/formatted.rs"
|
||||
mv "$tmpdir/formatted.rs" "$generated"
|
||||
@@ -8,7 +8,6 @@ mod error;
|
||||
mod in_memory;
|
||||
mod live_thread;
|
||||
mod local;
|
||||
mod remote;
|
||||
mod store;
|
||||
mod types;
|
||||
|
||||
@@ -20,7 +19,6 @@ pub use live_thread::LiveThread;
|
||||
pub use live_thread::LiveThreadInitGuard;
|
||||
pub use local::LocalThreadStore;
|
||||
pub use local::LocalThreadStoreConfig;
|
||||
pub use remote::RemoteThreadStore;
|
||||
pub use store::ThreadStore;
|
||||
pub use types::AppendThreadItemsParams;
|
||||
pub use types::ArchiveThreadParams;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# Remote Thread Store
|
||||
|
||||
- The Rust protobuf output in `proto/codex.thread_store.v1.rs` is checked in.
|
||||
- Do not add build-time protobuf generation to `codex-thread-store` unless the Bazel/Cargo story is intentionally changed.
|
||||
- When `proto/codex.thread_store.v1.proto` changes, regenerate the Rust file manually and include both files in the same commit.
|
||||
|
||||
Run this from the repository root:
|
||||
|
||||
```sh
|
||||
./codex-rs/thread-store/scripts/generate-proto.sh
|
||||
```
|
||||
|
||||
The command requires `protoc` to be available on `PATH`.
|
||||
@@ -1,457 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use codex_git_utils::GitSha;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::GitInfo;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::ThreadMemoryMode;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
|
||||
use super::proto;
|
||||
use crate::GitInfoPatch;
|
||||
use crate::OptionalStringPatch;
|
||||
use crate::SortDirection;
|
||||
use crate::StoredThread;
|
||||
use crate::StoredThreadHistory;
|
||||
use crate::ThreadEventPersistenceMode;
|
||||
use crate::ThreadMetadataPatch;
|
||||
use crate::ThreadPersistenceMetadata;
|
||||
use crate::ThreadSortKey;
|
||||
use crate::ThreadStoreError;
|
||||
use crate::ThreadStoreResult;
|
||||
|
||||
pub(super) fn remote_status_to_error(status: tonic::Status) -> ThreadStoreError {
|
||||
match status.code() {
|
||||
tonic::Code::InvalidArgument => ThreadStoreError::InvalidRequest {
|
||||
message: status.message().to_string(),
|
||||
},
|
||||
tonic::Code::AlreadyExists | tonic::Code::FailedPrecondition | tonic::Code::Aborted => {
|
||||
ThreadStoreError::Conflict {
|
||||
message: status.message().to_string(),
|
||||
}
|
||||
}
|
||||
_ => ThreadStoreError::Internal {
|
||||
message: format!("remote thread store request failed: {status}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn remote_status_to_thread_error(
|
||||
status: tonic::Status,
|
||||
thread_id: ThreadId,
|
||||
) -> ThreadStoreError {
|
||||
if status.code() == tonic::Code::NotFound {
|
||||
return ThreadStoreError::ThreadNotFound { thread_id };
|
||||
}
|
||||
remote_status_to_error(status)
|
||||
}
|
||||
|
||||
pub(super) fn proto_thread_id_request(thread_id: ThreadId) -> proto::ThreadIdRequest {
|
||||
proto::ThreadIdRequest {
|
||||
thread_id: thread_id.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn proto_sort_key(sort_key: ThreadSortKey) -> proto::ThreadSortKey {
|
||||
match sort_key {
|
||||
ThreadSortKey::CreatedAt => proto::ThreadSortKey::CreatedAt,
|
||||
ThreadSortKey::UpdatedAt => proto::ThreadSortKey::UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn proto_sort_direction(sort_direction: SortDirection) -> proto::SortDirection {
|
||||
match sort_direction {
|
||||
SortDirection::Asc => proto::SortDirection::Asc,
|
||||
SortDirection::Desc => proto::SortDirection::Desc,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn proto_event_persistence_mode(
|
||||
mode: ThreadEventPersistenceMode,
|
||||
) -> proto::ThreadEventPersistenceMode {
|
||||
match mode {
|
||||
ThreadEventPersistenceMode::Limited => proto::ThreadEventPersistenceMode::Limited,
|
||||
ThreadEventPersistenceMode::Extended => proto::ThreadEventPersistenceMode::Extended,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn proto_session_source(source: &SessionSource) -> proto::SessionSource {
|
||||
match source {
|
||||
SessionSource::Cli => proto_source(proto::SessionSourceKind::Cli),
|
||||
SessionSource::VSCode => proto_source(proto::SessionSourceKind::Vscode),
|
||||
SessionSource::Exec => proto_source(proto::SessionSourceKind::Exec),
|
||||
SessionSource::Mcp => proto_source(proto::SessionSourceKind::AppServer),
|
||||
SessionSource::Custom(custom) => proto::SessionSource {
|
||||
kind: proto::SessionSourceKind::Custom.into(),
|
||||
custom: Some(custom.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
SessionSource::SubAgent(SubAgentSource::Review) => {
|
||||
proto_source(proto::SessionSourceKind::SubAgentReview)
|
||||
}
|
||||
SessionSource::SubAgent(SubAgentSource::Compact) => {
|
||||
proto_source(proto::SessionSourceKind::SubAgentCompact)
|
||||
}
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_path,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
}) => proto::SessionSource {
|
||||
kind: proto::SessionSourceKind::SubAgentThreadSpawn.into(),
|
||||
sub_agent_parent_thread_id: Some(parent_thread_id.to_string()),
|
||||
sub_agent_depth: Some(*depth),
|
||||
sub_agent_path: agent_path.as_ref().map(|path| path.as_str().to_string()),
|
||||
sub_agent_nickname: agent_nickname.clone(),
|
||||
sub_agent_role: agent_role.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
SessionSource::SubAgent(SubAgentSource::MemoryConsolidation) => {
|
||||
proto_source(proto::SessionSourceKind::SubAgentMemoryConsolidation)
|
||||
}
|
||||
SessionSource::SubAgent(SubAgentSource::Other(other)) => proto::SessionSource {
|
||||
kind: proto::SessionSourceKind::SubAgentOther.into(),
|
||||
sub_agent_other: Some(other.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
SessionSource::Internal(_) => proto_source(proto::SessionSourceKind::Unknown),
|
||||
SessionSource::Unknown => proto_source(proto::SessionSourceKind::Unknown),
|
||||
}
|
||||
}
|
||||
|
||||
fn proto_source(kind: proto::SessionSourceKind) -> proto::SessionSource {
|
||||
proto::SessionSource {
|
||||
kind: kind.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn serialize_json<T: serde::Serialize>(
|
||||
value: &T,
|
||||
field_name: &str,
|
||||
) -> ThreadStoreResult<String> {
|
||||
serde_json::to_string(value).map_err(|err| ThreadStoreError::InvalidRequest {
|
||||
message: format!("failed to serialize {field_name} for remote thread store: {err}"),
|
||||
})
|
||||
}
|
||||
|
||||
fn deserialize_json<T: serde::de::DeserializeOwned>(
|
||||
json: &str,
|
||||
field_name: &str,
|
||||
) -> ThreadStoreResult<T> {
|
||||
serde_json::from_str(json).map_err(|err| ThreadStoreError::InvalidRequest {
|
||||
message: format!("remote thread store returned invalid {field_name}: {err}"),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn serialize_json_vec<T: serde::Serialize>(
|
||||
values: &[T],
|
||||
field_name: &str,
|
||||
) -> ThreadStoreResult<Vec<String>> {
|
||||
values
|
||||
.iter()
|
||||
.map(|value| serialize_json(value, field_name))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn deserialize_json_vec<T: serde::de::DeserializeOwned>(
|
||||
values: &[String],
|
||||
field_name: &str,
|
||||
) -> ThreadStoreResult<Vec<T>> {
|
||||
values
|
||||
.iter()
|
||||
.map(|value| deserialize_json(value, field_name))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn base_instructions_json(
|
||||
base_instructions: &BaseInstructions,
|
||||
) -> ThreadStoreResult<String> {
|
||||
serialize_json(base_instructions, "base_instructions")
|
||||
}
|
||||
|
||||
pub(super) fn dynamic_tools_json(
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> ThreadStoreResult<Vec<String>> {
|
||||
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")
|
||||
}
|
||||
|
||||
pub(super) fn stored_thread_history_from_proto(
|
||||
history: proto::StoredThreadHistory,
|
||||
) -> ThreadStoreResult<StoredThreadHistory> {
|
||||
let thread_id = ThreadId::from_string(&history.thread_id).map_err(|err| {
|
||||
ThreadStoreError::InvalidRequest {
|
||||
message: format!("remote thread store returned invalid history thread_id: {err}"),
|
||||
}
|
||||
})?;
|
||||
Ok(StoredThreadHistory {
|
||||
thread_id,
|
||||
items: deserialize_json_vec(&history.items_json, "rollout_item")?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn proto_metadata_patch(patch: ThreadMetadataPatch) -> proto::ThreadMetadataPatch {
|
||||
proto::ThreadMetadataPatch {
|
||||
name: patch.name,
|
||||
memory_mode: patch.memory_mode.map(proto_memory_mode).map(Into::into),
|
||||
git_info: patch.git_info.map(proto_git_info_patch),
|
||||
}
|
||||
}
|
||||
|
||||
fn proto_memory_mode(memory_mode: ThreadMemoryMode) -> proto::ThreadMemoryMode {
|
||||
match memory_mode {
|
||||
ThreadMemoryMode::Enabled => proto::ThreadMemoryMode::Enabled,
|
||||
ThreadMemoryMode::Disabled => proto::ThreadMemoryMode::Disabled,
|
||||
}
|
||||
}
|
||||
|
||||
fn proto_git_info_patch(patch: GitInfoPatch) -> proto::GitInfoPatch {
|
||||
proto::GitInfoPatch {
|
||||
sha: Some(proto_optional_string_patch(patch.sha)),
|
||||
branch: Some(proto_optional_string_patch(patch.branch)),
|
||||
origin_url: Some(proto_optional_string_patch(patch.origin_url)),
|
||||
}
|
||||
}
|
||||
|
||||
fn proto_optional_string_patch(patch: OptionalStringPatch) -> proto::OptionalStringPatch {
|
||||
match patch {
|
||||
None => proto::OptionalStringPatch {
|
||||
kind: proto::OptionalStringPatchKind::Unset.into(),
|
||||
value: None,
|
||||
},
|
||||
Some(None) => proto::OptionalStringPatch {
|
||||
kind: proto::OptionalStringPatchKind::Clear.into(),
|
||||
value: None,
|
||||
},
|
||||
Some(Some(value)) => proto::OptionalStringPatch {
|
||||
kind: proto::OptionalStringPatchKind::Set.into(),
|
||||
value: Some(value),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn stored_thread_from_proto(
|
||||
thread: proto::StoredThread,
|
||||
) -> ThreadStoreResult<StoredThread> {
|
||||
// Keep this mapping boring: the proto mirrors StoredThread for remote-readable
|
||||
// summary fields, except for Rust domain types that cross gRPC as stable scalar
|
||||
// values. Local-only fields such as rollout_path intentionally stay local.
|
||||
let source = thread
|
||||
.source
|
||||
.as_ref()
|
||||
.map(session_source_from_proto)
|
||||
.transpose()?
|
||||
.unwrap_or(SessionSource::Unknown);
|
||||
let thread_id = ThreadId::from_string(&thread.thread_id).map_err(|err| {
|
||||
ThreadStoreError::InvalidRequest {
|
||||
message: format!("remote thread store returned invalid thread_id: {err}"),
|
||||
}
|
||||
})?;
|
||||
let forked_from_id = thread
|
||||
.forked_from_id
|
||||
.as_deref()
|
||||
.map(ThreadId::from_string)
|
||||
.transpose()
|
||||
.map_err(|err| ThreadStoreError::InvalidRequest {
|
||||
message: format!("remote thread store returned invalid forked_from_id: {err}"),
|
||||
})?;
|
||||
|
||||
Ok(StoredThread {
|
||||
thread_id,
|
||||
rollout_path: thread.rollout_path.map(PathBuf::from),
|
||||
forked_from_id,
|
||||
preview: thread.preview,
|
||||
name: thread.name,
|
||||
model_provider: thread.model_provider,
|
||||
model: thread.model,
|
||||
reasoning_effort: thread
|
||||
.reasoning_effort
|
||||
.as_deref()
|
||||
.map(parse_reasoning_effort)
|
||||
.transpose()?,
|
||||
created_at: datetime_from_unix(thread.created_at)?,
|
||||
updated_at: datetime_from_unix(thread.updated_at)?,
|
||||
archived_at: thread.archived_at.map(datetime_from_unix).transpose()?,
|
||||
cwd: PathBuf::from(thread.cwd),
|
||||
cli_version: thread.cli_version,
|
||||
source,
|
||||
thread_source: thread
|
||||
.thread_source
|
||||
.map(|thread_source| thread_source.parse::<ThreadSource>())
|
||||
.transpose()
|
||||
.map_err(|error| ThreadStoreError::Internal { message: error })?,
|
||||
agent_nickname: thread.agent_nickname,
|
||||
agent_role: thread.agent_role,
|
||||
agent_path: thread.agent_path,
|
||||
git_info: thread.git_info.map(git_info_from_proto),
|
||||
approval_mode: thread
|
||||
.approval_mode_json
|
||||
.as_deref()
|
||||
.map(|json| deserialize_json(json, "approval_mode"))
|
||||
.transpose()?
|
||||
.unwrap_or(AskForApproval::OnRequest),
|
||||
sandbox_policy: thread
|
||||
.sandbox_policy_json
|
||||
.as_deref()
|
||||
.map(|json| deserialize_json(json, "sandbox_policy"))
|
||||
.transpose()?
|
||||
.unwrap_or_else(SandboxPolicy::new_read_only_policy),
|
||||
token_usage: thread
|
||||
.token_usage_json
|
||||
.as_deref()
|
||||
.map(|json| deserialize_json(json, "token_usage"))
|
||||
.transpose()?,
|
||||
first_user_message: thread.first_user_message,
|
||||
history: thread
|
||||
.history
|
||||
.map(stored_thread_history_from_proto)
|
||||
.transpose()?,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn stored_thread_to_proto(thread: StoredThread) -> proto::StoredThread {
|
||||
proto::StoredThread {
|
||||
thread_id: thread.thread_id.to_string(),
|
||||
forked_from_id: thread.forked_from_id.map(|thread_id| thread_id.to_string()),
|
||||
preview: thread.preview,
|
||||
name: thread.name,
|
||||
model_provider: thread.model_provider,
|
||||
model: thread.model,
|
||||
created_at: thread.created_at.timestamp(),
|
||||
updated_at: thread.updated_at.timestamp(),
|
||||
archived_at: thread.archived_at.map(|timestamp| timestamp.timestamp()),
|
||||
cwd: thread.cwd.to_string_lossy().into_owned(),
|
||||
cli_version: thread.cli_version,
|
||||
source: Some(proto_session_source(&thread.source)),
|
||||
thread_source: thread.thread_source.map(|source| source.to_string()),
|
||||
git_info: thread.git_info.map(git_info_to_proto),
|
||||
agent_nickname: thread.agent_nickname,
|
||||
agent_role: thread.agent_role,
|
||||
agent_path: thread.agent_path,
|
||||
reasoning_effort: thread.reasoning_effort.map(|effort| effort.to_string()),
|
||||
first_user_message: thread.first_user_message,
|
||||
rollout_path: thread
|
||||
.rollout_path
|
||||
.map(|path| path.to_string_lossy().into_owned()),
|
||||
approval_mode_json: Some(serialize_json(&thread.approval_mode, "approval_mode").unwrap()),
|
||||
sandbox_policy_json: Some(
|
||||
serialize_json(&thread.sandbox_policy, "sandbox_policy").unwrap(),
|
||||
),
|
||||
token_usage_json: thread
|
||||
.token_usage
|
||||
.as_ref()
|
||||
.map(|usage| serialize_json(usage, "token_usage").unwrap()),
|
||||
history: thread.history.map(stored_thread_history_to_proto),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn stored_thread_history_to_proto(history: StoredThreadHistory) -> proto::StoredThreadHistory {
|
||||
proto::StoredThreadHistory {
|
||||
thread_id: history.thread_id.to_string(),
|
||||
items_json: rollout_items_json(&history.items).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn datetime_from_unix(timestamp: i64) -> ThreadStoreResult<DateTime<Utc>> {
|
||||
DateTime::from_timestamp(timestamp, 0).ok_or_else(|| ThreadStoreError::InvalidRequest {
|
||||
message: format!("remote thread store returned invalid timestamp: {timestamp}"),
|
||||
})
|
||||
}
|
||||
|
||||
fn session_source_from_proto(source: &proto::SessionSource) -> ThreadStoreResult<SessionSource> {
|
||||
let kind = proto::SessionSourceKind::try_from(source.kind).unwrap_or_default();
|
||||
Ok(match kind {
|
||||
proto::SessionSourceKind::Unknown => SessionSource::Unknown,
|
||||
proto::SessionSourceKind::Cli => SessionSource::Cli,
|
||||
proto::SessionSourceKind::Vscode => SessionSource::VSCode,
|
||||
proto::SessionSourceKind::Exec => SessionSource::Exec,
|
||||
proto::SessionSourceKind::AppServer => SessionSource::Mcp,
|
||||
proto::SessionSourceKind::Custom => {
|
||||
SessionSource::Custom(source.custom.clone().unwrap_or_default())
|
||||
}
|
||||
proto::SessionSourceKind::SubAgentReview => SessionSource::SubAgent(SubAgentSource::Review),
|
||||
proto::SessionSourceKind::SubAgentCompact => {
|
||||
SessionSource::SubAgent(SubAgentSource::Compact)
|
||||
}
|
||||
proto::SessionSourceKind::SubAgentThreadSpawn => {
|
||||
let parent_thread_id = source
|
||||
.sub_agent_parent_thread_id
|
||||
.as_deref()
|
||||
.map(ThreadId::from_string)
|
||||
.transpose()
|
||||
.map_err(|err| ThreadStoreError::InvalidRequest {
|
||||
message: format!(
|
||||
"remote thread store returned invalid sub-agent parent thread id: {err}"
|
||||
),
|
||||
})?
|
||||
.ok_or_else(|| ThreadStoreError::InvalidRequest {
|
||||
message: "remote thread store omitted sub-agent parent thread id".to_string(),
|
||||
})?;
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: source.sub_agent_depth.unwrap_or_default(),
|
||||
agent_path: source
|
||||
.sub_agent_path
|
||||
.clone()
|
||||
.map(AgentPath::from_string)
|
||||
.transpose()
|
||||
.map_err(|message| ThreadStoreError::InvalidRequest { message })?,
|
||||
agent_nickname: source.sub_agent_nickname.clone(),
|
||||
agent_role: source.sub_agent_role.clone(),
|
||||
})
|
||||
}
|
||||
proto::SessionSourceKind::SubAgentMemoryConsolidation => {
|
||||
SessionSource::SubAgent(SubAgentSource::MemoryConsolidation)
|
||||
}
|
||||
proto::SessionSourceKind::SubAgentOther => SessionSource::SubAgent(SubAgentSource::Other(
|
||||
source.sub_agent_other.clone().unwrap_or_default(),
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
fn git_info_from_proto(info: proto::GitInfo) -> GitInfo {
|
||||
GitInfo {
|
||||
commit_hash: info.sha.as_deref().map(GitSha::new),
|
||||
branch: info.branch,
|
||||
repository_url: info.origin_url,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn git_info_to_proto(info: GitInfo) -> proto::GitInfo {
|
||||
proto::GitInfo {
|
||||
sha: info.commit_hash.map(|sha| sha.0),
|
||||
branch: info.branch,
|
||||
origin_url: info.repository_url,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_reasoning_effort(value: &str) -> ThreadStoreResult<ReasoningEffort> {
|
||||
ReasoningEffort::from_str(value).map_err(|message| ThreadStoreError::InvalidRequest {
|
||||
message: format!("remote thread store returned {message}"),
|
||||
})
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
use super::RemoteThreadStore;
|
||||
use super::helpers::proto_session_source;
|
||||
use super::helpers::proto_sort_direction;
|
||||
use super::helpers::proto_sort_key;
|
||||
use super::helpers::remote_status_to_error;
|
||||
use super::helpers::stored_thread_from_proto;
|
||||
use super::proto;
|
||||
use crate::ListThreadsParams;
|
||||
use crate::ThreadPage;
|
||||
use crate::ThreadStoreError;
|
||||
use crate::ThreadStoreResult;
|
||||
|
||||
pub(super) async fn list_threads(
|
||||
store: &RemoteThreadStore,
|
||||
params: ListThreadsParams,
|
||||
) -> ThreadStoreResult<ThreadPage> {
|
||||
let request = proto::ListThreadsRequest {
|
||||
page_size: params
|
||||
.page_size
|
||||
.try_into()
|
||||
.map_err(|_| ThreadStoreError::InvalidRequest {
|
||||
message: format!("page_size is too large: {}", params.page_size),
|
||||
})?,
|
||||
cursor: params.cursor,
|
||||
sort_key: proto_sort_key(params.sort_key).into(),
|
||||
sort_direction: proto_sort_direction(params.sort_direction).into(),
|
||||
allowed_sources: params
|
||||
.allowed_sources
|
||||
.iter()
|
||||
.map(proto_session_source)
|
||||
.collect(),
|
||||
model_provider_filter: params
|
||||
.model_providers
|
||||
.map(|values| proto::ModelProviderFilter { values }),
|
||||
cwd_filter: params.cwd_filters.map(|values| proto::CwdFilter {
|
||||
values: values
|
||||
.into_iter()
|
||||
.map(|cwd| cwd.display().to_string())
|
||||
.collect(),
|
||||
}),
|
||||
archived: params.archived,
|
||||
search_term: params.search_term,
|
||||
use_state_db_only: params.use_state_db_only,
|
||||
};
|
||||
|
||||
let response = store
|
||||
.client()
|
||||
.await?
|
||||
.list_threads(request)
|
||||
.await
|
||||
.map_err(remote_status_to_error)?
|
||||
.into_inner();
|
||||
|
||||
let items = response
|
||||
.threads
|
||||
.into_iter()
|
||||
.map(stored_thread_from_proto)
|
||||
.collect::<ThreadStoreResult<Vec<_>>>()?;
|
||||
|
||||
Ok(ThreadPage {
|
||||
items,
|
||||
next_cursor: response.next_cursor,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tonic::Request;
|
||||
use tonic::Response;
|
||||
use tonic::Status;
|
||||
use tonic::transport::Server;
|
||||
|
||||
use super::super::helpers::stored_thread_to_proto;
|
||||
use super::super::proto::thread_store_server;
|
||||
use super::super::proto::thread_store_server::ThreadStoreServer;
|
||||
use super::*;
|
||||
use crate::ThreadSortKey;
|
||||
use crate::ThreadStore;
|
||||
|
||||
#[derive(Default)]
|
||||
struct TestServer;
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl thread_store_server::ThreadStore for TestServer {
|
||||
async fn list_threads(
|
||||
&self,
|
||||
request: Request<proto::ListThreadsRequest>,
|
||||
) -> Result<Response<proto::ListThreadsResponse>, Status> {
|
||||
let request = request.into_inner();
|
||||
assert_eq!(request.page_size, 2);
|
||||
assert_eq!(request.cursor.as_deref(), Some("cursor-1"));
|
||||
assert_eq!(
|
||||
proto::ThreadSortKey::try_from(request.sort_key),
|
||||
Ok(proto::ThreadSortKey::UpdatedAt)
|
||||
);
|
||||
assert_eq!(
|
||||
proto::SortDirection::try_from(request.sort_direction),
|
||||
Ok(proto::SortDirection::Desc)
|
||||
);
|
||||
assert_eq!(request.archived, true);
|
||||
assert_eq!(request.search_term.as_deref(), Some("needle"));
|
||||
assert!(request.use_state_db_only);
|
||||
assert_eq!(
|
||||
request.model_provider_filter,
|
||||
Some(proto::ModelProviderFilter {
|
||||
values: vec!["openai".to_string()],
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
request.cwd_filter,
|
||||
Some(proto::CwdFilter {
|
||||
values: vec!["/workspace".to_string()],
|
||||
})
|
||||
);
|
||||
assert_eq!(request.allowed_sources.len(), 1);
|
||||
assert_eq!(
|
||||
proto::SessionSourceKind::try_from(request.allowed_sources[0].kind),
|
||||
Ok(proto::SessionSourceKind::Cli)
|
||||
);
|
||||
|
||||
Ok(Response::new(proto::ListThreadsResponse {
|
||||
threads: vec![proto::StoredThread {
|
||||
thread_id: "11111111-1111-1111-1111-111111111111".to_string(),
|
||||
forked_from_id: None,
|
||||
preview: "hello".to_string(),
|
||||
name: Some("named thread".to_string()),
|
||||
model_provider: "openai".to_string(),
|
||||
model: Some("gpt-5".to_string()),
|
||||
created_at: 100,
|
||||
updated_at: 200,
|
||||
archived_at: Some(300),
|
||||
cwd: "/workspace".to_string(),
|
||||
cli_version: "1.2.3".to_string(),
|
||||
source: Some(proto::SessionSource {
|
||||
kind: proto::SessionSourceKind::Cli.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
thread_source: Some("user".to_string()),
|
||||
git_info: Some(proto::GitInfo {
|
||||
sha: Some("abc123".to_string()),
|
||||
branch: Some("main".to_string()),
|
||||
origin_url: Some("https://example.test/repo.git".to_string()),
|
||||
}),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
agent_path: None,
|
||||
reasoning_effort: Some("medium".to_string()),
|
||||
first_user_message: Some("hello".to_string()),
|
||||
rollout_path: None,
|
||||
approval_mode_json: None,
|
||||
sandbox_policy_json: None,
|
||||
token_usage_json: None,
|
||||
history: None,
|
||||
}],
|
||||
next_cursor: Some("cursor-2".to_string()),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_threads_calls_remote_service() {
|
||||
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");
|
||||
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
|
||||
let server = tokio::spawn(async move {
|
||||
Server::builder()
|
||||
.add_service(ThreadStoreServer::new(TestServer))
|
||||
.serve_with_incoming_shutdown(
|
||||
tokio_stream::wrappers::TcpListenerStream::new(listener),
|
||||
async {
|
||||
let _ = shutdown_rx.await;
|
||||
},
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
||||
let store = RemoteThreadStore::new(format!("http://{addr}"));
|
||||
let page = store
|
||||
.list_threads(ListThreadsParams {
|
||||
page_size: 2,
|
||||
cursor: Some("cursor-1".to_string()),
|
||||
sort_key: ThreadSortKey::UpdatedAt,
|
||||
sort_direction: crate::SortDirection::Desc,
|
||||
allowed_sources: vec![SessionSource::Cli],
|
||||
model_providers: Some(vec!["openai".to_string()]),
|
||||
cwd_filters: Some(vec![PathBuf::from("/workspace")]),
|
||||
archived: true,
|
||||
search_term: Some("needle".to_string()),
|
||||
use_state_db_only: true,
|
||||
})
|
||||
.await
|
||||
.expect("list threads");
|
||||
|
||||
assert_eq!(page.next_cursor.as_deref(), Some("cursor-2"));
|
||||
assert_eq!(page.items.len(), 1);
|
||||
let item = &page.items[0];
|
||||
assert_eq!(
|
||||
item.thread_id.to_string(),
|
||||
"11111111-1111-1111-1111-111111111111"
|
||||
);
|
||||
assert_eq!(item.name.as_deref(), Some("named thread"));
|
||||
assert_eq!(item.preview, "hello");
|
||||
assert_eq!(item.first_user_message.as_deref(), Some("hello"));
|
||||
assert_eq!(item.model_provider, "openai");
|
||||
assert_eq!(item.model.as_deref(), Some("gpt-5"));
|
||||
assert_eq!(item.created_at.timestamp(), 100);
|
||||
assert_eq!(item.updated_at.timestamp(), 200);
|
||||
assert_eq!(item.archived_at.map(|ts| ts.timestamp()), Some(300));
|
||||
assert_eq!(item.cwd, PathBuf::from("/workspace"));
|
||||
assert_eq!(item.cli_version, "1.2.3");
|
||||
assert_eq!(item.source, SessionSource::Cli);
|
||||
assert_eq!(item.reasoning_effort, Some(ReasoningEffort::Medium));
|
||||
assert_eq!(
|
||||
item.git_info.as_ref().and_then(|git| git.branch.as_deref()),
|
||||
Some("main")
|
||||
);
|
||||
|
||||
let _ = shutdown_tx.send(());
|
||||
server.await.expect("join server").expect("server");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stored_thread_proto_roundtrips_through_domain_type() {
|
||||
let thread = proto::StoredThread {
|
||||
thread_id: "11111111-1111-1111-1111-111111111111".to_string(),
|
||||
forked_from_id: Some("22222222-2222-2222-2222-222222222222".to_string()),
|
||||
preview: "preview text".to_string(),
|
||||
name: Some("named thread".to_string()),
|
||||
model_provider: "openai".to_string(),
|
||||
model: Some("gpt-5".to_string()),
|
||||
created_at: 100,
|
||||
updated_at: 200,
|
||||
archived_at: Some(300),
|
||||
cwd: "/workspace/project".to_string(),
|
||||
cli_version: "1.2.3".to_string(),
|
||||
source: Some(proto::SessionSource {
|
||||
kind: proto::SessionSourceKind::SubAgentThreadSpawn.into(),
|
||||
sub_agent_parent_thread_id: Some(
|
||||
"33333333-3333-3333-3333-333333333333".to_string(),
|
||||
),
|
||||
sub_agent_depth: Some(2),
|
||||
sub_agent_path: Some("/root/review/backend".to_string()),
|
||||
sub_agent_nickname: Some("Navigator".to_string()),
|
||||
sub_agent_role: Some("explorer".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
thread_source: Some("subagent".to_string()),
|
||||
git_info: Some(proto::GitInfo {
|
||||
sha: Some("abc123".to_string()),
|
||||
branch: Some("main".to_string()),
|
||||
origin_url: Some("https://example.test/repo.git".to_string()),
|
||||
}),
|
||||
agent_nickname: Some("Navigator".to_string()),
|
||||
agent_role: Some("explorer".to_string()),
|
||||
agent_path: Some("/root/review/backend".to_string()),
|
||||
reasoning_effort: Some("high".to_string()),
|
||||
first_user_message: Some("first message".to_string()),
|
||||
rollout_path: None,
|
||||
approval_mode_json: None,
|
||||
sandbox_policy_json: None,
|
||||
token_usage_json: None,
|
||||
history: None,
|
||||
};
|
||||
|
||||
let stored = stored_thread_from_proto(thread.clone()).expect("proto to stored thread");
|
||||
|
||||
assert_eq!(stored.rollout_path, None);
|
||||
assert!(stored.history.is_none());
|
||||
let roundtripped = stored_thread_to_proto(stored);
|
||||
assert_eq!(roundtripped.thread_id, thread.thread_id);
|
||||
assert_eq!(roundtripped.forked_from_id, thread.forked_from_id);
|
||||
assert_eq!(roundtripped.source, thread.source);
|
||||
assert_eq!(roundtripped.git_info, thread.git_info);
|
||||
}
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
mod helpers;
|
||||
mod list_threads;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use codex_protocol::ThreadId;
|
||||
|
||||
use crate::AppendThreadItemsParams;
|
||||
use crate::ArchiveThreadParams;
|
||||
use crate::CreateThreadParams;
|
||||
use crate::ListThreadsParams;
|
||||
use crate::LoadThreadHistoryParams;
|
||||
use crate::ReadThreadByRolloutPathParams;
|
||||
use crate::ReadThreadParams;
|
||||
use crate::ResumeThreadParams;
|
||||
use crate::StoredThread;
|
||||
use crate::StoredThreadHistory;
|
||||
use crate::ThreadPage;
|
||||
use crate::ThreadStore;
|
||||
use crate::ThreadStoreError;
|
||||
use crate::ThreadStoreResult;
|
||||
use crate::UpdateThreadMetadataParams;
|
||||
use proto::thread_store_client::ThreadStoreClient;
|
||||
|
||||
#[path = "proto/codex.thread_store.v1.rs"]
|
||||
mod proto;
|
||||
|
||||
/// gRPC-backed [`ThreadStore`] implementation for deployments whose durable thread data lives
|
||||
/// outside the app-server process.
|
||||
///
|
||||
/// This store is still a work in progress: app-server code should call the generic
|
||||
/// [`ThreadStore`] methods, and unsupported remote operations will return explicit
|
||||
/// `not_implemented` errors until the remote API catches up.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RemoteThreadStore {
|
||||
endpoint: String,
|
||||
}
|
||||
|
||||
impl RemoteThreadStore {
|
||||
pub fn new(endpoint: impl Into<String>) -> Self {
|
||||
Self {
|
||||
endpoint: endpoint.into(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn client(&self) -> ThreadStoreResult<ThreadStoreClient<tonic::transport::Channel>> {
|
||||
ThreadStoreClient::connect(self.endpoint.clone())
|
||||
.await
|
||||
.map_err(|err| ThreadStoreError::Internal {
|
||||
message: format!("failed to connect to remote thread store: {err}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ThreadStore for RemoteThreadStore {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
async fn create_thread(&self, params: CreateThreadParams) -> ThreadStoreResult<()> {
|
||||
let thread_id = params.thread_id;
|
||||
let request = proto::CreateThreadRequest {
|
||||
thread_id: thread_id.to_string(),
|
||||
forked_from_id: params.forked_from_id.map(|thread_id| thread_id.to_string()),
|
||||
source: Some(helpers::proto_session_source(¶ms.source)),
|
||||
base_instructions_json: helpers::base_instructions_json(¶ms.base_instructions)?,
|
||||
dynamic_tools_json: helpers::dynamic_tools_json(¶ms.dynamic_tools)?,
|
||||
event_persistence_mode: helpers::proto_event_persistence_mode(
|
||||
params.event_persistence_mode,
|
||||
)
|
||||
.into(),
|
||||
metadata_json: helpers::thread_persistence_metadata_json(¶ms.metadata)?,
|
||||
};
|
||||
self.client()
|
||||
.await?
|
||||
.create_thread(request)
|
||||
.await
|
||||
.map_err(|status| helpers::remote_status_to_thread_error(status, thread_id))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn resume_thread(&self, params: ResumeThreadParams) -> ThreadStoreResult<()> {
|
||||
let thread_id = params.thread_id;
|
||||
let (has_history, history_json) = match params.history {
|
||||
Some(history) => (true, helpers::rollout_items_json(&history)?),
|
||||
None => (false, Vec::new()),
|
||||
};
|
||||
let request = proto::ResumeThreadRequest {
|
||||
thread_id: thread_id.to_string(),
|
||||
rollout_path: params
|
||||
.rollout_path
|
||||
.map(|path| path.to_string_lossy().into_owned()),
|
||||
history_json,
|
||||
has_history,
|
||||
include_archived: params.include_archived,
|
||||
event_persistence_mode: helpers::proto_event_persistence_mode(
|
||||
params.event_persistence_mode,
|
||||
)
|
||||
.into(),
|
||||
metadata_json: helpers::thread_persistence_metadata_json(¶ms.metadata)?,
|
||||
};
|
||||
self.client()
|
||||
.await?
|
||||
.resume_thread(request)
|
||||
.await
|
||||
.map_err(|status| helpers::remote_status_to_thread_error(status, thread_id))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn append_items(&self, params: AppendThreadItemsParams) -> ThreadStoreResult<()> {
|
||||
let thread_id = params.thread_id;
|
||||
let request = proto::AppendThreadItemsRequest {
|
||||
thread_id: thread_id.to_string(),
|
||||
items_json: helpers::rollout_items_json(¶ms.items)?,
|
||||
};
|
||||
self.client()
|
||||
.await?
|
||||
.append_items(request)
|
||||
.await
|
||||
.map_err(|status| helpers::remote_status_to_thread_error(status, thread_id))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn persist_thread(&self, thread_id: ThreadId) -> ThreadStoreResult<()> {
|
||||
self.client()
|
||||
.await?
|
||||
.persist_thread(helpers::proto_thread_id_request(thread_id))
|
||||
.await
|
||||
.map_err(|status| helpers::remote_status_to_thread_error(status, thread_id))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn flush_thread(&self, thread_id: ThreadId) -> ThreadStoreResult<()> {
|
||||
self.client()
|
||||
.await?
|
||||
.flush_thread(helpers::proto_thread_id_request(thread_id))
|
||||
.await
|
||||
.map_err(|status| helpers::remote_status_to_thread_error(status, thread_id))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown_thread(&self, thread_id: ThreadId) -> ThreadStoreResult<()> {
|
||||
self.client()
|
||||
.await?
|
||||
.shutdown_thread(helpers::proto_thread_id_request(thread_id))
|
||||
.await
|
||||
.map_err(|status| helpers::remote_status_to_thread_error(status, thread_id))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn discard_thread(&self, thread_id: ThreadId) -> ThreadStoreResult<()> {
|
||||
self.client()
|
||||
.await?
|
||||
.discard_thread(helpers::proto_thread_id_request(thread_id))
|
||||
.await
|
||||
.map_err(|status| helpers::remote_status_to_thread_error(status, thread_id))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_history(
|
||||
&self,
|
||||
params: LoadThreadHistoryParams,
|
||||
) -> ThreadStoreResult<StoredThreadHistory> {
|
||||
let thread_id = params.thread_id;
|
||||
let response = self
|
||||
.client()
|
||||
.await?
|
||||
.load_history(proto::LoadThreadHistoryRequest {
|
||||
thread_id: thread_id.to_string(),
|
||||
include_archived: params.include_archived,
|
||||
})
|
||||
.await
|
||||
.map_err(|status| helpers::remote_status_to_thread_error(status, thread_id))?
|
||||
.into_inner();
|
||||
helpers::stored_thread_history_from_proto(response)
|
||||
}
|
||||
|
||||
async fn read_thread(&self, params: ReadThreadParams) -> ThreadStoreResult<StoredThread> {
|
||||
let thread_id = params.thread_id;
|
||||
let response = self
|
||||
.client()
|
||||
.await?
|
||||
.read_thread(proto::ReadThreadRequest {
|
||||
thread_id: thread_id.to_string(),
|
||||
include_archived: params.include_archived,
|
||||
include_history: params.include_history,
|
||||
})
|
||||
.await
|
||||
.map_err(|status| helpers::remote_status_to_thread_error(status, thread_id))?
|
||||
.into_inner();
|
||||
let thread = response.thread.ok_or_else(|| ThreadStoreError::Internal {
|
||||
message: "remote thread store omitted read_thread response thread".to_string(),
|
||||
})?;
|
||||
helpers::stored_thread_from_proto(thread)
|
||||
}
|
||||
|
||||
async fn read_thread_by_rollout_path(
|
||||
&self,
|
||||
_params: ReadThreadByRolloutPathParams,
|
||||
) -> ThreadStoreResult<StoredThread> {
|
||||
Err(ThreadStoreError::Internal {
|
||||
message: "remote thread store does not support read_thread_by_rollout_path".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn list_threads(&self, params: ListThreadsParams) -> ThreadStoreResult<ThreadPage> {
|
||||
list_threads::list_threads(self, params).await
|
||||
}
|
||||
|
||||
async fn update_thread_metadata(
|
||||
&self,
|
||||
params: UpdateThreadMetadataParams,
|
||||
) -> ThreadStoreResult<StoredThread> {
|
||||
let thread_id = params.thread_id;
|
||||
let response = self
|
||||
.client()
|
||||
.await?
|
||||
.update_thread_metadata(proto::UpdateThreadMetadataRequest {
|
||||
thread_id: thread_id.to_string(),
|
||||
patch: Some(helpers::proto_metadata_patch(params.patch)),
|
||||
include_archived: params.include_archived,
|
||||
})
|
||||
.await
|
||||
.map_err(|status| helpers::remote_status_to_thread_error(status, thread_id))?
|
||||
.into_inner();
|
||||
let thread = response.thread.ok_or_else(|| ThreadStoreError::Internal {
|
||||
message: "remote thread store omitted update_thread_metadata response thread"
|
||||
.to_string(),
|
||||
})?;
|
||||
helpers::stored_thread_from_proto(thread)
|
||||
}
|
||||
|
||||
async fn archive_thread(&self, params: ArchiveThreadParams) -> ThreadStoreResult<()> {
|
||||
let thread_id = params.thread_id;
|
||||
self.client()
|
||||
.await?
|
||||
.archive_thread(proto::ArchiveThreadRequest {
|
||||
thread_id: thread_id.to_string(),
|
||||
})
|
||||
.await
|
||||
.map_err(|status| helpers::remote_status_to_thread_error(status, thread_id))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn unarchive_thread(
|
||||
&self,
|
||||
params: ArchiveThreadParams,
|
||||
) -> ThreadStoreResult<StoredThread> {
|
||||
let thread_id = params.thread_id;
|
||||
let response = self
|
||||
.client()
|
||||
.await?
|
||||
.unarchive_thread(proto::ArchiveThreadRequest {
|
||||
thread_id: thread_id.to_string(),
|
||||
})
|
||||
.await
|
||||
.map_err(|status| helpers::remote_status_to_thread_error(status, thread_id))?
|
||||
.into_inner();
|
||||
let thread = response.thread.ok_or_else(|| ThreadStoreError::Internal {
|
||||
message: "remote thread store omitted unarchive_thread response thread".to_string(),
|
||||
})?;
|
||||
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,
|
||||
thread_source: None,
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package codex.thread_store.v1;
|
||||
|
||||
service ThreadStore {
|
||||
rpc CreateThread(CreateThreadRequest) returns (Empty);
|
||||
rpc ResumeThread(ResumeThreadRequest) returns (Empty);
|
||||
rpc AppendItems(AppendThreadItemsRequest) returns (Empty);
|
||||
rpc PersistThread(ThreadIdRequest) returns (Empty);
|
||||
rpc FlushThread(ThreadIdRequest) returns (Empty);
|
||||
rpc ShutdownThread(ThreadIdRequest) returns (Empty);
|
||||
rpc DiscardThread(ThreadIdRequest) returns (Empty);
|
||||
rpc LoadHistory(LoadThreadHistoryRequest) returns (StoredThreadHistory);
|
||||
rpc ReadThread(ReadThreadRequest) returns (StoredThreadResponse);
|
||||
rpc ListThreads(ListThreadsRequest) returns (ListThreadsResponse);
|
||||
rpc UpdateThreadMetadata(UpdateThreadMetadataRequest) returns (StoredThreadResponse);
|
||||
rpc ArchiveThread(ArchiveThreadRequest) returns (Empty);
|
||||
rpc UnarchiveThread(ArchiveThreadRequest) returns (StoredThreadResponse);
|
||||
}
|
||||
|
||||
message Empty {}
|
||||
|
||||
message ThreadIdRequest {
|
||||
string thread_id = 1;
|
||||
}
|
||||
|
||||
message CreateThreadRequest {
|
||||
string thread_id = 1;
|
||||
optional string forked_from_id = 2;
|
||||
SessionSource source = 3;
|
||||
string base_instructions_json = 4;
|
||||
repeated string dynamic_tools_json = 5;
|
||||
ThreadEventPersistenceMode event_persistence_mode = 6;
|
||||
string metadata_json = 7;
|
||||
}
|
||||
|
||||
message ResumeThreadRequest {
|
||||
string thread_id = 1;
|
||||
optional string rollout_path = 2;
|
||||
repeated string history_json = 3;
|
||||
bool has_history = 4;
|
||||
bool include_archived = 5;
|
||||
ThreadEventPersistenceMode event_persistence_mode = 6;
|
||||
string metadata_json = 7;
|
||||
}
|
||||
|
||||
message AppendThreadItemsRequest {
|
||||
string thread_id = 1;
|
||||
repeated string items_json = 2;
|
||||
}
|
||||
|
||||
message LoadThreadHistoryRequest {
|
||||
string thread_id = 1;
|
||||
bool include_archived = 2;
|
||||
}
|
||||
|
||||
message ReadThreadRequest {
|
||||
string thread_id = 1;
|
||||
bool include_archived = 2;
|
||||
bool include_history = 3;
|
||||
}
|
||||
|
||||
message ListThreadsRequest {
|
||||
uint32 page_size = 1;
|
||||
optional string cursor = 2;
|
||||
ThreadSortKey sort_key = 3;
|
||||
repeated SessionSource allowed_sources = 4;
|
||||
optional ModelProviderFilter model_provider_filter = 5;
|
||||
bool archived = 6;
|
||||
optional string search_term = 7;
|
||||
optional CwdFilter cwd_filter = 8;
|
||||
bool use_state_db_only = 9;
|
||||
SortDirection sort_direction = 10;
|
||||
}
|
||||
|
||||
message ModelProviderFilter {
|
||||
repeated string values = 1;
|
||||
}
|
||||
|
||||
message CwdFilter {
|
||||
repeated string values = 1;
|
||||
}
|
||||
|
||||
enum ThreadSortKey {
|
||||
THREAD_SORT_KEY_CREATED_AT = 0;
|
||||
THREAD_SORT_KEY_UPDATED_AT = 1;
|
||||
}
|
||||
|
||||
enum SortDirection {
|
||||
SORT_DIRECTION_ASC = 0;
|
||||
SORT_DIRECTION_DESC = 1;
|
||||
}
|
||||
|
||||
message ListThreadsResponse {
|
||||
repeated StoredThread threads = 1;
|
||||
optional string next_cursor = 2;
|
||||
}
|
||||
|
||||
message StoredThreadResponse {
|
||||
StoredThread thread = 1;
|
||||
}
|
||||
|
||||
message StoredThreadHistory {
|
||||
string thread_id = 1;
|
||||
repeated string items_json = 2;
|
||||
}
|
||||
|
||||
message StoredThread {
|
||||
// Mirrors Rust's StoredThread. Domain types that are not protobuf-native,
|
||||
// such as ThreadId, DateTime<Utc>, and PathBuf, are represented as their
|
||||
// stable scalar forms on the wire.
|
||||
string thread_id = 1;
|
||||
optional string forked_from_id = 2;
|
||||
string preview = 3;
|
||||
optional string name = 4;
|
||||
string model_provider = 5;
|
||||
optional string model = 6;
|
||||
int64 created_at = 7;
|
||||
int64 updated_at = 8;
|
||||
optional int64 archived_at = 9;
|
||||
string cwd = 10;
|
||||
string cli_version = 11;
|
||||
SessionSource source = 12;
|
||||
optional GitInfo git_info = 13;
|
||||
optional string agent_nickname = 14;
|
||||
optional string agent_role = 15;
|
||||
optional string agent_path = 16;
|
||||
optional string reasoning_effort = 17;
|
||||
optional string first_user_message = 18;
|
||||
optional string rollout_path = 19;
|
||||
optional string approval_mode_json = 20;
|
||||
optional string sandbox_policy_json = 21;
|
||||
optional string token_usage_json = 22;
|
||||
optional StoredThreadHistory history = 23;
|
||||
optional string thread_source = 24;
|
||||
}
|
||||
|
||||
message SessionSource {
|
||||
SessionSourceKind kind = 1;
|
||||
optional string custom = 2;
|
||||
optional string sub_agent_parent_thread_id = 3;
|
||||
optional int32 sub_agent_depth = 4;
|
||||
optional string sub_agent_other = 5;
|
||||
optional string sub_agent_path = 6;
|
||||
optional string sub_agent_nickname = 7;
|
||||
optional string sub_agent_role = 8;
|
||||
}
|
||||
|
||||
enum SessionSourceKind {
|
||||
SESSION_SOURCE_KIND_UNKNOWN = 0;
|
||||
SESSION_SOURCE_KIND_CLI = 1;
|
||||
SESSION_SOURCE_KIND_VSCODE = 2;
|
||||
SESSION_SOURCE_KIND_EXEC = 3;
|
||||
SESSION_SOURCE_KIND_APP_SERVER = 4;
|
||||
SESSION_SOURCE_KIND_CUSTOM = 5;
|
||||
SESSION_SOURCE_KIND_SUB_AGENT_REVIEW = 6;
|
||||
SESSION_SOURCE_KIND_SUB_AGENT_COMPACT = 7;
|
||||
SESSION_SOURCE_KIND_SUB_AGENT_THREAD_SPAWN = 8;
|
||||
SESSION_SOURCE_KIND_SUB_AGENT_MEMORY_CONSOLIDATION = 9;
|
||||
SESSION_SOURCE_KIND_SUB_AGENT_OTHER = 10;
|
||||
}
|
||||
|
||||
message GitInfo {
|
||||
optional string sha = 1;
|
||||
optional string branch = 2;
|
||||
optional string origin_url = 3;
|
||||
}
|
||||
|
||||
message UpdateThreadMetadataRequest {
|
||||
string thread_id = 1;
|
||||
ThreadMetadataPatch patch = 2;
|
||||
bool include_archived = 3;
|
||||
}
|
||||
|
||||
message ThreadMetadataPatch {
|
||||
optional string name = 1;
|
||||
optional ThreadMemoryMode memory_mode = 2;
|
||||
optional GitInfoPatch git_info = 3;
|
||||
}
|
||||
|
||||
enum ThreadMemoryMode {
|
||||
THREAD_MEMORY_MODE_ENABLED = 0;
|
||||
THREAD_MEMORY_MODE_DISABLED = 1;
|
||||
}
|
||||
|
||||
message GitInfoPatch {
|
||||
OptionalStringPatch sha = 1;
|
||||
OptionalStringPatch branch = 2;
|
||||
OptionalStringPatch origin_url = 3;
|
||||
}
|
||||
|
||||
message OptionalStringPatch {
|
||||
OptionalStringPatchKind kind = 1;
|
||||
optional string value = 2;
|
||||
}
|
||||
|
||||
enum OptionalStringPatchKind {
|
||||
OPTIONAL_STRING_PATCH_KIND_UNSET = 0;
|
||||
OPTIONAL_STRING_PATCH_KIND_CLEAR = 1;
|
||||
OPTIONAL_STRING_PATCH_KIND_SET = 2;
|
||||
}
|
||||
|
||||
message ArchiveThreadRequest {
|
||||
string thread_id = 1;
|
||||
}
|
||||
|
||||
enum ThreadEventPersistenceMode {
|
||||
THREAD_EVENT_PERSISTENCE_MODE_LIMITED = 0;
|
||||
THREAD_EVENT_PERSISTENCE_MODE_EXTENDED = 1;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user