From 6e72f0dbfd5e6b875e1ce00e8079e2bc15e28fe4 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 16 Apr 2026 10:15:31 -0700 Subject: [PATCH] [codex] Add remote thread store implementation (#17826) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add a "remote" thread store implementation - Implement the remote thread store as a thin wrapper that makes grpc calls to a configurable service endpoint - Implement only the thread/list method to start - Encode the grpc method/param shape as protobufs in the remote implementation A wart: the proto generation script is an "example" binary target. This is an example target only because Cargo lets examples use dev-dependencies, which keeps tonic-prost-build out of the normal codex-thread-store dependency surface. A regular bin would either need to add proto generation deps as normal runtime deps, or use a feature-gated optional dep, which this repo’s manifest checks explicitly reject. --- MODULE.bazel.lock | 4 + codex-rs/Cargo.lock | 70 ++- codex-rs/Cargo.toml | 2 + codex-rs/thread-store/Cargo.toml | 12 +- .../thread-store/examples/generate-proto.rs | 14 + .../thread-store/scripts/generate-proto.sh | 38 ++ codex-rs/thread-store/src/lib.rs | 2 + codex-rs/thread-store/src/remote/AGENTS.md | 13 + codex-rs/thread-store/src/remote/helpers.rs | 255 ++++++++++ .../thread-store/src/remote/list_threads.rs | 243 +++++++++ codex-rs/thread-store/src/remote/mod.rs | 112 +++++ .../remote/proto/codex.thread_store.v1.proto | 86 ++++ .../src/remote/proto/codex.thread_store.v1.rs | 460 ++++++++++++++++++ 13 files changed, 1307 insertions(+), 4 deletions(-) create mode 100644 codex-rs/thread-store/examples/generate-proto.rs create mode 100755 codex-rs/thread-store/scripts/generate-proto.sh create mode 100644 codex-rs/thread-store/src/remote/AGENTS.md create mode 100644 codex-rs/thread-store/src/remote/helpers.rs create mode 100644 codex-rs/thread-store/src/remote/list_threads.rs create mode 100644 codex-rs/thread-store/src/remote/mod.rs create mode 100644 codex-rs/thread-store/src/remote/proto/codex.thread_store.v1.proto create mode 100644 codex-rs/thread-store/src/remote/proto/codex.thread_store.v1.rs diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 5d7572f46..ef80693c7 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1203,9 +1203,11 @@ "process-wrap_9.0.1": "{\"dependencies\":[{\"name\":\"futures\",\"optional\":true,\"req\":\"^0.3.30\"},{\"name\":\"indexmap\",\"req\":\"^2.9.0\"},{\"default_features\":false,\"features\":[\"fs\",\"poll\",\"signal\"],\"name\":\"nix\",\"optional\":true,\"req\":\"^0.30.1\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"remoteprocess\",\"req\":\"^0.5.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.20.0\"},{\"features\":[\"io-util\",\"macros\",\"process\",\"rt\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.38.2\"},{\"features\":[\"io-util\",\"macros\",\"process\",\"rt\",\"rt-multi-thread\",\"time\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.38.2\"},{\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.40\"},{\"name\":\"windows\",\"optional\":true,\"req\":\"^0.62.2\",\"target\":\"cfg(windows)\"}],\"features\":{\"creation-flags\":[\"dep:windows\",\"windows/Win32_System_Threading\"],\"default\":[\"creation-flags\",\"job-object\",\"kill-on-drop\",\"process-group\",\"process-session\",\"tracing\"],\"job-object\":[\"dep:windows\",\"windows/Win32_Security\",\"windows/Win32_System_Diagnostics_ToolHelp\",\"windows/Win32_System_IO\",\"windows/Win32_System_JobObjects\",\"windows/Win32_System_Threading\"],\"kill-on-drop\":[],\"process-group\":[],\"process-session\":[\"process-group\"],\"reset-sigmask\":[],\"std\":[\"dep:nix\"],\"tokio1\":[\"dep:nix\",\"dep:futures\",\"dep:tokio\"],\"tracing\":[\"dep:tracing\"]}}", "proptest_1.9.0": "{\"dependencies\":[{\"name\":\"bit-set\",\"optional\":true,\"req\":\"^0.8.0\"},{\"name\":\"bit-vec\",\"optional\":true,\"req\":\"^0.8.0\"},{\"name\":\"bitflags\",\"req\":\"^2.9\"},{\"default_features\":false,\"name\":\"num-traits\",\"req\":\"^0.2.15\"},{\"name\":\"proptest-macro\",\"optional\":true,\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"rand\",\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"rand_chacha\",\"req\":\"^0.9\"},{\"name\":\"rand_xorshift\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.0\"},{\"name\":\"regex-syntax\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rusty-fork\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"tempfile\",\"optional\":true,\"req\":\"^3.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"=1.0.112\"},{\"name\":\"unarray\",\"req\":\"^0.1.4\"},{\"name\":\"x86\",\"optional\":true,\"req\":\"^0.52.0\"}],\"features\":{\"alloc\":[],\"atomic64bit\":[],\"attr-macro\":[\"proptest-macro\"],\"bit-set\":[\"dep:bit-set\",\"dep:bit-vec\"],\"default\":[\"std\",\"fork\",\"timeout\",\"bit-set\"],\"default-code-coverage\":[\"std\",\"fork\",\"timeout\",\"bit-set\"],\"fork\":[\"std\",\"rusty-fork\",\"tempfile\"],\"handle-panics\":[\"std\"],\"hardware-rng\":[\"x86\"],\"no_std\":[\"num-traits/libm\"],\"std\":[\"rand/std\",\"rand/os_rng\",\"regex-syntax\",\"num-traits/std\"],\"timeout\":[\"fork\",\"rusty-fork/timeout\"],\"unstable\":[]}}", "prost-build_0.12.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"name\":\"heck\",\"req\":\">=0.4, <=0.5\"},{\"default_features\":false,\"features\":[\"use_alloc\"],\"name\":\"itertools\",\"req\":\">=0.10, <=0.12\"},{\"name\":\"log\",\"req\":\"^0.4.4\"},{\"default_features\":false,\"name\":\"multimap\",\"req\":\">=0.8, <=0.10\"},{\"name\":\"once_cell\",\"req\":\"^1.17.1\"},{\"default_features\":false,\"name\":\"petgraph\",\"req\":\"^0.6\"},{\"name\":\"prettyplease\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"prost\",\"req\":\"^0.12.6\"},{\"default_features\":false,\"name\":\"prost-types\",\"req\":\"^0.12.6\"},{\"default_features\":false,\"name\":\"pulldown-cmark\",\"optional\":true,\"req\":\"^0.9.1\"},{\"name\":\"pulldown-cmark-to-cmark\",\"optional\":true,\"req\":\"^10.0.1\"},{\"default_features\":false,\"features\":[\"std\",\"unicode-bool\"],\"name\":\"regex\",\"req\":\"^1.8.1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"cleanup-markdown\":[\"dep:pulldown-cmark\",\"dep:pulldown-cmark-to-cmark\"],\"default\":[\"format\"],\"format\":[\"dep:prettyplease\",\"dep:syn\"]}}", + "prost-build_0.14.3": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"heck\",\"req\":\">=0.4, <=0.5\"},{\"default_features\":false,\"features\":[\"use_alloc\"],\"name\":\"itertools\",\"req\":\">=0.10, <=0.14\"},{\"name\":\"log\",\"req\":\"^0.4.4\"},{\"default_features\":false,\"name\":\"multimap\",\"req\":\">=0.8, <=0.10\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"petgraph\",\"req\":\"^0.8\"},{\"name\":\"prettyplease\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"prost\",\"req\":\"^0.14.3\"},{\"default_features\":false,\"name\":\"prost-types\",\"req\":\"^0.14.3\"},{\"default_features\":false,\"name\":\"pulldown-cmark\",\"optional\":true,\"req\":\"^0.13\"},{\"name\":\"pulldown-cmark-to-cmark\",\"optional\":true,\"req\":\"^22\"},{\"default_features\":false,\"features\":[\"std\",\"unicode-bool\"],\"name\":\"regex\",\"req\":\"^1.8.1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"cleanup-markdown\":[\"dep:pulldown-cmark\",\"dep:pulldown-cmark-to-cmark\"],\"default\":[\"format\"],\"format\":[\"dep:prettyplease\",\"dep:syn\"]}}", "prost-derive_0.12.6": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.1\"},{\"default_features\":false,\"features\":[\"use_alloc\"],\"name\":\"itertools\",\"req\":\">=0.10, <=0.12\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", "prost-derive_0.14.3": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.1\"},{\"name\":\"itertools\",\"req\":\">=0.10.1, <=0.14\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", "prost-types_0.12.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"prost-derive\"],\"name\":\"prost\",\"req\":\"^0.12.6\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"prost/std\"]}}", + "prost-types_0.14.3": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.4\"},{\"default_features\":false,\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"prost\",\"req\":\"^0.14.3\"}],\"features\":{\"arbitrary\":[\"dep:arbitrary\"],\"default\":[\"std\"],\"std\":[\"prost/std\"]}}", "prost_0.12.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"prost-derive\",\"optional\":true,\"req\":\"^0.12.6\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"default\":[\"derive\",\"std\"],\"derive\":[\"dep:prost-derive\"],\"no-recursion-limit\":[],\"prost-derive\":[\"derive\"],\"std\":[]}}", "prost_0.14.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"prost-derive\",\"optional\":true,\"req\":\"^0.14.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"}],\"features\":{\"default\":[\"derive\",\"std\"],\"derive\":[\"dep:prost-derive\"],\"no-recursion-limit\":[],\"std\":[]}}", "psl-types_2.0.11": "{\"dependencies\":[],\"features\":{}}", @@ -1459,6 +1461,8 @@ "toml_parser_1.0.6+spec-1.1.0": "{\"dependencies\":[{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.20\"},{\"features\":[\"test\"],\"kind\":\"dev\",\"name\":\"anstream\",\"req\":\"^0.6.20\"},{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"default_features\":false,\"name\":\"winnow\",\"req\":\"^0.7.13\"}],\"features\":{\"alloc\":[],\"debug\":[\"std\",\"dep:anstream\",\"dep:anstyle\"],\"default\":[\"std\"],\"simd\":[\"winnow/simd\"],\"std\":[\"alloc\"],\"unsafe\":[]}}", "toml_parser_1.0.9+spec-1.1.0": "{\"dependencies\":[{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.20\"},{\"features\":[\"test\"],\"kind\":\"dev\",\"name\":\"anstream\",\"req\":\"^0.6.20\"},{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"default_features\":false,\"name\":\"winnow\",\"req\":\"^0.7.13\"}],\"features\":{\"alloc\":[],\"debug\":[\"std\",\"dep:anstream\",\"dep:anstyle\"],\"default\":[\"std\"],\"simd\":[\"winnow/simd\"],\"std\":[\"alloc\"],\"unsafe\":[]}}", "toml_writer_1.0.6+spec-1.1.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.7.0\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"kind\":\"dev\",\"name\":\"toml_old\",\"package\":\"toml\",\"req\":\"^0.5.11\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", + "tonic-build_0.14.3": "{\"dependencies\":[{\"name\":\"prettyplease\",\"req\":\"^0.2\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{\"default\":[\"transport\"],\"transport\":[]}}", + "tonic-prost-build_0.14.3": "{\"dependencies\":[{\"name\":\"prettyplease\",\"req\":\"^0.2\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"prost-build\",\"req\":\"^0.14\"},{\"name\":\"prost-types\",\"req\":\"^0.14\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"name\":\"syn\",\"req\":\"^2.0\"},{\"name\":\"tempfile\",\"req\":\"^3.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"tonic\",\"req\":\"^0.14.0\"},{\"default_features\":false,\"name\":\"tonic-build\",\"req\":\"^0.14.0\"}],\"features\":{\"cleanup-markdown\":[\"prost-build/cleanup-markdown\"],\"default\":[\"transport\",\"cleanup-markdown\"],\"transport\":[\"tonic-build/transport\"]}}", "tonic-prost_0.14.3": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"http-body\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"name\":\"prost\",\"req\":\"^0.14\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"tonic\",\"req\":\"^0.14.0\"}],\"features\":{}}", "tonic_0.14.3": "{\"dependencies\":[{\"name\":\"async-trait\",\"optional\":true,\"req\":\"^0.1.13\"},{\"default_features\":false,\"name\":\"axum\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"http\",\"req\":\"^1.1.0\"},{\"name\":\"http-body\",\"req\":\"^1\"},{\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"features\":[\"http1\",\"http2\"],\"name\":\"hyper\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"hyper-timeout\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"tokio\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.11\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project\",\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8\"},{\"features\":[\"all\"],\"name\":\"socket2\",\"optional\":true,\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.0\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.2\"},{\"default_features\":false,\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"logging\",\"tls12\"],\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26.1\"},{\"default_features\":false,\"name\":\"tokio-stream\",\"req\":\"^0.1.16\"},{\"default_features\":false,\"name\":\"tower\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"load-shed\",\"timeout\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5\"},{\"name\":\"tower-layer\",\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"zstd\",\"optional\":true,\"req\":\"^0.13.0\"}],\"features\":{\"_tls-any\":[\"dep:tokio\",\"tokio?/rt\",\"tokio?/macros\",\"tls-connect-info\"],\"channel\":[\"dep:hyper\",\"hyper?/client\",\"dep:hyper-util\",\"hyper-util?/client-legacy\",\"dep:tower\",\"tower?/balance\",\"tower?/buffer\",\"tower?/discover\",\"tower?/limit\",\"tower?/load-shed\",\"tower?/util\",\"dep:tokio\",\"tokio?/time\",\"dep:hyper-timeout\"],\"codegen\":[\"dep:async-trait\"],\"default\":[\"router\",\"transport\",\"codegen\"],\"deflate\":[\"dep:flate2\"],\"gzip\":[\"dep:flate2\"],\"router\":[\"dep:axum\",\"dep:tower\",\"tower?/util\"],\"server\":[\"dep:h2\",\"dep:hyper\",\"hyper?/server\",\"dep:hyper-util\",\"hyper-util?/service\",\"hyper-util?/server-auto\",\"dep:socket2\",\"dep:tokio\",\"tokio?/macros\",\"tokio?/net\",\"tokio?/time\",\"tokio-stream/net\",\"dep:tower\",\"tower?/util\",\"tower?/limit\",\"tower?/load-shed\"],\"tls-aws-lc\":[\"_tls-any\",\"tokio-rustls/aws-lc-rs\"],\"tls-connect-info\":[\"dep:tokio-rustls\"],\"tls-native-roots\":[\"_tls-any\",\"channel\",\"dep:rustls-native-certs\"],\"tls-ring\":[\"_tls-any\",\"tokio-rustls/ring\"],\"tls-webpki-roots\":[\"_tls-any\",\"channel\",\"dep:webpki-roots\"],\"transport\":[\"server\",\"channel\"],\"zstd\":[\"dep:zstd\"]}}", "tower-http_0.6.8": "{\"dependencies\":[{\"features\":[\"tokio\"],\"name\":\"async-compression\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"bitflags\",\"req\":\"^2.0.2\"},{\"kind\":\"dev\",\"name\":\"brotli\",\"req\":\"^8\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.14\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.14\"},{\"name\":\"http\",\"req\":\"^1.0\"},{\"name\":\"http-body\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"http-range-header\",\"optional\":true,\"req\":\"^0.4.0\"},{\"name\":\"httpdate\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"client-legacy\",\"http1\",\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"name\":\"iri-string\",\"optional\":true,\"req\":\"^0.7.0\"},{\"default_features\":false,\"name\":\"mime\",\"optional\":true,\"req\":\"^0.3.17\"},{\"default_features\":false,\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1\"},{\"name\":\"percent-encoding\",\"optional\":true,\"req\":\"^2.1.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"sync_wrapper\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.6\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"io\"],\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"tower\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"buffer\",\"util\",\"retry\",\"make\",\"timeout\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"zstd\",\"req\":\"^0.13\"}],\"features\":{\"add-extension\":[],\"auth\":[\"base64\",\"validate-request\"],\"catch-panic\":[\"tracing\",\"futures-util/std\",\"dep:http-body\",\"dep:http-body-util\"],\"compression-br\":[\"async-compression/brotli\",\"futures-core\",\"dep:http-body\",\"tokio-util\",\"tokio\"],\"compression-deflate\":[\"async-compression/zlib\",\"futures-core\",\"dep:http-body\",\"tokio-util\",\"tokio\"],\"compression-full\":[\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"compression-zstd\"],\"compression-gzip\":[\"async-compression/gzip\",\"futures-core\",\"dep:http-body\",\"tokio-util\",\"tokio\"],\"compression-zstd\":[\"async-compression/zstd\",\"futures-core\",\"dep:http-body\",\"tokio-util\",\"tokio\"],\"cors\":[],\"decompression-br\":[\"async-compression/brotli\",\"futures-core\",\"dep:http-body\",\"dep:http-body-util\",\"tokio-util\",\"tokio\"],\"decompression-deflate\":[\"async-compression/zlib\",\"futures-core\",\"dep:http-body\",\"dep:http-body-util\",\"tokio-util\",\"tokio\"],\"decompression-full\":[\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"decompression-zstd\"],\"decompression-gzip\":[\"async-compression/gzip\",\"futures-core\",\"dep:http-body\",\"dep:http-body-util\",\"tokio-util\",\"tokio\"],\"decompression-zstd\":[\"async-compression/zstd\",\"futures-core\",\"dep:http-body\",\"dep:http-body-util\",\"tokio-util\",\"tokio\"],\"default\":[],\"follow-redirect\":[\"futures-util\",\"dep:http-body\",\"iri-string\",\"tower/util\"],\"fs\":[\"futures-core\",\"futures-util\",\"dep:http-body\",\"dep:http-body-util\",\"tokio/fs\",\"tokio-util/io\",\"tokio/io-util\",\"dep:http-range-header\",\"mime_guess\",\"mime\",\"percent-encoding\",\"httpdate\",\"set-status\",\"futures-util/alloc\",\"tracing\"],\"full\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-full\",\"cors\",\"decompression-full\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"limit\":[\"dep:http-body\",\"dep:http-body-util\"],\"map-request-body\":[],\"map-response-body\":[],\"metrics\":[\"dep:http-body\",\"tokio/time\"],\"normalize-path\":[],\"propagate-header\":[],\"redirect\":[],\"request-id\":[\"uuid\"],\"sensitive-headers\":[],\"set-header\":[],\"set-status\":[],\"timeout\":[\"dep:http-body\",\"tokio/time\"],\"trace\":[\"dep:http-body\",\"tracing\"],\"util\":[\"tower\"],\"validate-request\":[\"mime\"]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 1be5b45b6..3e21398f8 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2886,11 +2886,16 @@ 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", "uuid", ] @@ -7561,7 +7566,7 @@ dependencies = [ "heck 0.4.1", "itertools 0.11.0", "prost 0.12.6", - "prost-types", + "prost-types 0.12.6", ] [[package]] @@ -7575,7 +7580,7 @@ dependencies = [ "pbjson", "pbjson-build", "prost 0.12.6", - "prost-build", + "prost-build 0.12.6", "serde", ] @@ -7999,7 +8004,26 @@ dependencies = [ "petgraph 0.6.5", "prettyplease", "prost 0.12.6", - "prost-types", + "prost-types 0.12.6", + "regex", + "syn 2.0.114", + "tempfile", +] + +[[package]] +name = "prost-build" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +dependencies = [ + "heck 0.5.0", + "itertools 0.14.0", + "log", + "multimap", + "petgraph 0.8.3", + "prettyplease", + "prost 0.14.3", + "prost-types 0.14.3", "regex", "syn 2.0.114", "tempfile", @@ -8040,6 +8064,15 @@ dependencies = [ "prost 0.12.6", ] +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost 0.14.3", +] + [[package]] name = "psl" version = "2.1.184" @@ -10948,8 +10981,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a286e33f82f8a1ee2df63f4fa35c0becf4a85a0cb03091a15fd7bf0b402dc94a" dependencies = [ "async-trait", + "axum", "base64 0.22.1", "bytes", + "h2", "http 1.4.0", "http-body", "http-body-util", @@ -10959,6 +10994,7 @@ dependencies = [ "percent-encoding", "pin-project", "rustls-native-certs", + "socket2 0.6.2", "sync_wrapper", "tokio", "tokio-rustls", @@ -10969,6 +11005,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic-build" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27aac809edf60b741e2d7db6367214d078856b8a5bff0087e94ff330fb97b6fc" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "tonic-prost" version = "0.14.3" @@ -10980,6 +11028,22 @@ dependencies = [ "tonic", ] +[[package]] +name = "tonic-prost-build" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4556786613791cfef4ed134aa670b61a85cfcacf71543ef33e8d801abae988f" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build 0.14.3", + "prost-types 0.14.3", + "quote", + "syn 2.0.114", + "tempfile", + "tonic-build", +] + [[package]] name = "tower" version = "0.5.3" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index e7867304e..deb373881 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -347,6 +347,8 @@ tracing-appender = "0.2.3" tracing-opentelemetry = "0.32.0" tracing-subscriber = "0.3.22" tracing-test = "0.2.5" +tonic = { version = "0.14.3", default-features = false, features = ["channel", "codegen"] } +tonic-prost = "0.14.3" tree-sitter = "0.25.10" tree-sitter-bash = "0.25" ts-rs = "11" diff --git a/codex-rs/thread-store/Cargo.toml b/codex-rs/thread-store/Cargo.toml index 328d9a4a7..3698332b9 100644 --- a/codex-rs/thread-store/Cargo.toml +++ b/codex-rs/thread-store/Cargo.toml @@ -8,6 +8,10 @@ version.workspace = true name = "codex_thread_store" path = "src/lib.rs" +[[example]] +name = "generate-proto" +path = "examples/generate-proto.rs" + [lints] workspace = true @@ -17,13 +21,19 @@ chrono = { workspace = true, features = ["serde"] } codex-git-utils = { workspace = true } codex-protocol = { workspace = true } codex-rollout = { workspace = true } +prost = "0.14.3" serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } +tonic = { workspace = true } +tonic-prost = { workspace = true } [dev-dependencies] codex-state = { workspace = true } pretty_assertions = { workspace = true } tempfile = { workspace = true } -tokio = { workspace = true, features = ["macros"] } +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 } diff --git a/codex-rs/thread-store/examples/generate-proto.rs b/codex-rs/thread-store/examples/generate-proto.rs new file mode 100644 index 000000000..aa159f5bc --- /dev/null +++ b/codex-rs/thread-store/examples/generate-proto.rs @@ -0,0 +1,14 @@ +use std::path::PathBuf; + +fn main() -> Result<(), Box> { + let proto_dir = PathBuf::from(std::env::args().nth(1).expect("proto dir")); + 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(()) +} diff --git a/codex-rs/thread-store/scripts/generate-proto.sh b/codex-rs/thread-store/scripts/generate-proto.sh new file mode 100755 index 000000000..4045467ca --- /dev/null +++ b/codex-rs/thread-store/scripts/generate-proto.sh @@ -0,0 +1,38 @@ +#!/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" diff --git a/codex-rs/thread-store/src/lib.rs b/codex-rs/thread-store/src/lib.rs index a64bf023b..f8dc94908 100644 --- a/codex-rs/thread-store/src/lib.rs +++ b/codex-rs/thread-store/src/lib.rs @@ -7,6 +7,7 @@ mod error; mod local; mod recorder; +mod remote; mod store; mod types; @@ -14,6 +15,7 @@ pub use error::ThreadStoreError; pub use error::ThreadStoreResult; pub use local::LocalThreadStore; pub use recorder::ThreadRecorder; +pub use remote::RemoteThreadStore; pub use store::ThreadStore; pub use types::AppendThreadItemsParams; pub use types::ArchiveThreadParams; diff --git a/codex-rs/thread-store/src/remote/AGENTS.md b/codex-rs/thread-store/src/remote/AGENTS.md new file mode 100644 index 000000000..b2b2b6417 --- /dev/null +++ b/codex-rs/thread-store/src/remote/AGENTS.md @@ -0,0 +1,13 @@ +# 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`. diff --git a/codex-rs/thread-store/src/remote/helpers.rs b/codex-rs/thread-store/src/remote/helpers.rs new file mode 100644 index 000000000..c02889e16 --- /dev/null +++ b/codex-rs/thread-store/src/remote/helpers.rs @@ -0,0 +1,255 @@ +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::openai_models::ReasoningEffort; +use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::GitInfo; +use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::protocol::SessionSource; +use codex_protocol::protocol::SubAgentSource; + +use super::proto; +use crate::StoredThread; +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 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_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::Unknown => proto_source(proto::SessionSourceKind::Unknown), + } +} + +fn proto_source(kind: proto::SessionSourceKind) -> proto::SessionSource { + proto::SessionSource { + kind: kind.into(), + ..Default::default() + } +} + +pub(super) fn stored_thread_from_proto( + thread: proto::StoredThread, +) -> ThreadStoreResult { + // 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: None, + 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, + 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: AskForApproval::OnRequest, + sandbox_policy: SandboxPolicy::new_read_only_policy(), + token_usage: None, + first_user_message: thread.first_user_message, + history: None, + }) +} + +#[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)), + 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, + } +} + +fn datetime_from_unix(timestamp: i64) -> ThreadStoreResult> { + 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 { + 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::from_str(value).map_err(|message| ThreadStoreError::InvalidRequest { + message: format!("remote thread store returned {message}"), + }) +} diff --git a/codex-rs/thread-store/src/remote/list_threads.rs b/codex-rs/thread-store/src/remote/list_threads.rs new file mode 100644 index 000000000..b94db9dc8 --- /dev/null +++ b/codex-rs/thread-store/src/remote/list_threads.rs @@ -0,0 +1,243 @@ +use super::RemoteThreadStore; +use super::helpers::proto_session_source; +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 { + 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(), + allowed_sources: params + .allowed_sources + .iter() + .map(proto_session_source) + .collect(), + model_provider_filter: params + .model_providers + .map(|values| proto::ModelProviderFilter { values }), + archived: params.archived, + search_term: params.search_term, + }; + + 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::>>()?; + + 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, + ) -> Result, 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!(request.archived, true); + assert_eq!(request.search_term.as_deref(), Some("needle")); + assert_eq!( + request.model_provider_filter, + Some(proto::ModelProviderFilter { + values: vec!["openai".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() + }), + 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()), + }], + 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, + allowed_sources: vec![SessionSource::Cli], + model_providers: Some(vec!["openai".to_string()]), + archived: true, + search_term: Some("needle".to_string()), + }) + .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() + }), + 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()), + }; + + let stored = stored_thread_from_proto(thread.clone()).expect("proto to stored thread"); + + assert_eq!(stored.rollout_path, None); + assert!(stored.history.is_none()); + assert_eq!(stored_thread_to_proto(stored), thread); + } +} diff --git a/codex-rs/thread-store/src/remote/mod.rs b/codex-rs/thread-store/src/remote/mod.rs new file mode 100644 index 000000000..2e8b9e5af --- /dev/null +++ b/codex-rs/thread-store/src/remote/mod.rs @@ -0,0 +1,112 @@ +mod helpers; +mod list_threads; + +use async_trait::async_trait; + +use crate::AppendThreadItemsParams; +use crate::ArchiveThreadParams; +use crate::CreateThreadParams; +use crate::ListThreadsParams; +use crate::LoadThreadHistoryParams; +use crate::ReadThreadParams; +use crate::ResumeThreadRecorderParams; +use crate::SetThreadNameParams; +use crate::StoredThread; +use crate::StoredThreadHistory; +use crate::ThreadPage; +use crate::ThreadRecorder; +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. +#[derive(Clone, Debug)] +pub struct RemoteThreadStore { + endpoint: String, +} + +impl RemoteThreadStore { + pub fn new(endpoint: impl Into) -> Self { + Self { + endpoint: endpoint.into(), + } + } + + async fn client(&self) -> ThreadStoreResult> { + 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 { + async fn create_thread( + &self, + _params: CreateThreadParams, + ) -> ThreadStoreResult> { + Err(not_implemented("create_thread")) + } + + async fn resume_thread_recorder( + &self, + _params: ResumeThreadRecorderParams, + ) -> ThreadStoreResult> { + Err(not_implemented("resume_thread_recorder")) + } + + async fn append_items(&self, _params: AppendThreadItemsParams) -> ThreadStoreResult<()> { + Err(not_implemented("append_items")) + } + + async fn load_history( + &self, + _params: LoadThreadHistoryParams, + ) -> ThreadStoreResult { + Err(not_implemented("load_history")) + } + + async fn read_thread(&self, _params: ReadThreadParams) -> ThreadStoreResult { + Err(not_implemented("read_thread")) + } + + async fn list_threads(&self, params: ListThreadsParams) -> ThreadStoreResult { + list_threads::list_threads(self, params).await + } + + async fn set_thread_name(&self, _params: SetThreadNameParams) -> ThreadStoreResult<()> { + Err(not_implemented("set_thread_name")) + } + + async fn update_thread_metadata( + &self, + _params: UpdateThreadMetadataParams, + ) -> ThreadStoreResult { + Err(not_implemented("update_thread_metadata")) + } + + async fn archive_thread(&self, _params: ArchiveThreadParams) -> ThreadStoreResult<()> { + Err(not_implemented("archive_thread")) + } + + async fn unarchive_thread( + &self, + _params: ArchiveThreadParams, + ) -> ThreadStoreResult { + Err(not_implemented("unarchive_thread")) + } +} + +fn not_implemented(method: &str) -> ThreadStoreError { + ThreadStoreError::Internal { + message: format!("remote thread store does not implement {method} yet"), + } +} diff --git a/codex-rs/thread-store/src/remote/proto/codex.thread_store.v1.proto b/codex-rs/thread-store/src/remote/proto/codex.thread_store.v1.proto new file mode 100644 index 000000000..f217027fc --- /dev/null +++ b/codex-rs/thread-store/src/remote/proto/codex.thread_store.v1.proto @@ -0,0 +1,86 @@ +syntax = "proto3"; + +package codex.thread_store.v1; + +service ThreadStore { + rpc ListThreads(ListThreadsRequest) returns (ListThreadsResponse); +} + +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; +} + +message ModelProviderFilter { + repeated string values = 1; +} + +enum ThreadSortKey { + THREAD_SORT_KEY_CREATED_AT = 0; + THREAD_SORT_KEY_UPDATED_AT = 1; +} + +message ListThreadsResponse { + repeated StoredThread threads = 1; + optional string next_cursor = 2; +} + +message StoredThread { + // Mirrors Rust's StoredThread. Domain types that are not protobuf-native, + // such as ThreadId, DateTime, 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; +} + +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; +} diff --git a/codex-rs/thread-store/src/remote/proto/codex.thread_store.v1.rs b/codex-rs/thread-store/src/remote/proto/codex.thread_store.v1.rs new file mode 100644 index 000000000..6d0e41026 --- /dev/null +++ b/codex-rs/thread-store/src/remote/proto/codex.thread_store.v1.rs @@ -0,0 +1,460 @@ +// This file is @generated by prost-build. +#![allow(clippy::trivially_copy_pass_by_ref)] + +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListThreadsRequest { + #[prost(uint32, tag = "1")] + pub page_size: u32, + #[prost(string, optional, tag = "2")] + pub cursor: ::core::option::Option<::prost::alloc::string::String>, + #[prost(enumeration = "ThreadSortKey", tag = "3")] + pub sort_key: i32, + #[prost(message, repeated, tag = "4")] + pub allowed_sources: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "5")] + pub model_provider_filter: ::core::option::Option, + #[prost(bool, tag = "6")] + pub archived: bool, + #[prost(string, optional, tag = "7")] + pub search_term: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ModelProviderFilter { + #[prost(string, repeated, tag = "1")] + pub values: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListThreadsResponse { + #[prost(message, repeated, tag = "1")] + pub threads: ::prost::alloc::vec::Vec, + #[prost(string, optional, tag = "2")] + pub next_cursor: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct StoredThread { + /// Mirrors Rust's StoredThread. Domain types that are not protobuf-native, + /// such as ThreadId, DateTime, and PathBuf, are represented as their + /// stable scalar forms on the wire. + #[prost(string, tag = "1")] + pub thread_id: ::prost::alloc::string::String, + #[prost(string, optional, tag = "2")] + pub forked_from_id: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, tag = "3")] + pub preview: ::prost::alloc::string::String, + #[prost(string, optional, tag = "4")] + pub name: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, tag = "5")] + pub model_provider: ::prost::alloc::string::String, + #[prost(string, optional, tag = "6")] + pub model: ::core::option::Option<::prost::alloc::string::String>, + #[prost(int64, tag = "7")] + pub created_at: i64, + #[prost(int64, tag = "8")] + pub updated_at: i64, + #[prost(int64, optional, tag = "9")] + pub archived_at: ::core::option::Option, + #[prost(string, tag = "10")] + pub cwd: ::prost::alloc::string::String, + #[prost(string, tag = "11")] + pub cli_version: ::prost::alloc::string::String, + #[prost(message, optional, tag = "12")] + pub source: ::core::option::Option, + #[prost(message, optional, tag = "13")] + pub git_info: ::core::option::Option, + #[prost(string, optional, tag = "14")] + pub agent_nickname: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "15")] + pub agent_role: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "16")] + pub agent_path: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "17")] + pub reasoning_effort: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "18")] + pub first_user_message: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct SessionSource { + #[prost(enumeration = "SessionSourceKind", tag = "1")] + pub kind: i32, + #[prost(string, optional, tag = "2")] + pub custom: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "3")] + pub sub_agent_parent_thread_id: ::core::option::Option<::prost::alloc::string::String>, + #[prost(int32, optional, tag = "4")] + pub sub_agent_depth: ::core::option::Option, + #[prost(string, optional, tag = "5")] + pub sub_agent_other: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "6")] + pub sub_agent_path: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "7")] + pub sub_agent_nickname: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "8")] + pub sub_agent_role: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct GitInfo { + #[prost(string, optional, tag = "1")] + pub sha: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "2")] + pub branch: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "3")] + pub origin_url: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ThreadSortKey { + CreatedAt = 0, + UpdatedAt = 1, +} +impl ThreadSortKey { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::CreatedAt => "THREAD_SORT_KEY_CREATED_AT", + Self::UpdatedAt => "THREAD_SORT_KEY_UPDATED_AT", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "THREAD_SORT_KEY_CREATED_AT" => Some(Self::CreatedAt), + "THREAD_SORT_KEY_UPDATED_AT" => Some(Self::UpdatedAt), + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum SessionSourceKind { + Unknown = 0, + Cli = 1, + Vscode = 2, + Exec = 3, + AppServer = 4, + Custom = 5, + SubAgentReview = 6, + SubAgentCompact = 7, + SubAgentThreadSpawn = 8, + SubAgentMemoryConsolidation = 9, + SubAgentOther = 10, +} +impl SessionSourceKind { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unknown => "SESSION_SOURCE_KIND_UNKNOWN", + Self::Cli => "SESSION_SOURCE_KIND_CLI", + Self::Vscode => "SESSION_SOURCE_KIND_VSCODE", + Self::Exec => "SESSION_SOURCE_KIND_EXEC", + Self::AppServer => "SESSION_SOURCE_KIND_APP_SERVER", + Self::Custom => "SESSION_SOURCE_KIND_CUSTOM", + Self::SubAgentReview => "SESSION_SOURCE_KIND_SUB_AGENT_REVIEW", + Self::SubAgentCompact => "SESSION_SOURCE_KIND_SUB_AGENT_COMPACT", + Self::SubAgentThreadSpawn => "SESSION_SOURCE_KIND_SUB_AGENT_THREAD_SPAWN", + Self::SubAgentMemoryConsolidation => { + "SESSION_SOURCE_KIND_SUB_AGENT_MEMORY_CONSOLIDATION" + } + Self::SubAgentOther => "SESSION_SOURCE_KIND_SUB_AGENT_OTHER", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "SESSION_SOURCE_KIND_UNKNOWN" => Some(Self::Unknown), + "SESSION_SOURCE_KIND_CLI" => Some(Self::Cli), + "SESSION_SOURCE_KIND_VSCODE" => Some(Self::Vscode), + "SESSION_SOURCE_KIND_EXEC" => Some(Self::Exec), + "SESSION_SOURCE_KIND_APP_SERVER" => Some(Self::AppServer), + "SESSION_SOURCE_KIND_CUSTOM" => Some(Self::Custom), + "SESSION_SOURCE_KIND_SUB_AGENT_REVIEW" => Some(Self::SubAgentReview), + "SESSION_SOURCE_KIND_SUB_AGENT_COMPACT" => Some(Self::SubAgentCompact), + "SESSION_SOURCE_KIND_SUB_AGENT_THREAD_SPAWN" => Some(Self::SubAgentThreadSpawn), + "SESSION_SOURCE_KIND_SUB_AGENT_MEMORY_CONSOLIDATION" => { + Some(Self::SubAgentMemoryConsolidation) + } + "SESSION_SOURCE_KIND_SUB_AGENT_OTHER" => Some(Self::SubAgentOther), + _ => None, + } + } +} +/// Generated client implementations. +pub mod thread_store_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value + )] + use tonic::codegen::http::Uri; + use tonic::codegen::*; + #[derive(Debug, Clone)] + pub struct ThreadStoreClient { + inner: tonic::client::Grpc, + } + impl ThreadStoreClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl ThreadStoreClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> ThreadStoreClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + >>::Error: + Into + std::marker::Send + std::marker::Sync, + { + ThreadStoreClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn list_threads( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/codex.thread_store.v1.ThreadStore/ListThreads", + ); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new( + "codex.thread_store.v1.ThreadStore", + "ListThreads", + )); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod thread_store_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with ThreadStoreServer. + #[async_trait] + pub trait ThreadStore: std::marker::Send + std::marker::Sync + 'static { + async fn list_threads( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + } + #[derive(Debug)] + pub struct ThreadStoreServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl ThreadStoreServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for ThreadStoreServer + where + T: ThreadStore, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/codex.thread_store.v1.ThreadStore/ListThreads" => { + #[allow(non_camel_case_types)] + struct ListThreadsSvc(pub Arc); + impl tonic::server::UnaryService for ListThreadsSvc { + type Response = super::ListThreadsResponse; + type Future = BoxFuture, tonic::Status>; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::list_threads(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ListThreadsSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => Box::pin(async move { + let mut response = http::Response::new(tonic::body::Body::default()); + let headers = response.headers_mut(); + headers.insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers.insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }), + } + } + } + impl Clone for ThreadStoreServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "codex.thread_store.v1.ThreadStore"; + impl tonic::server::NamedService for ThreadStoreServer { + const NAME: &'static str = SERVICE_NAME; + } +}