diff --git a/.bazelignore b/.bazelignore index eda018aeb2..2e90753cb8 100644 --- a/.bazelignore +++ b/.bazelignore @@ -1,3 +1,4 @@ # Without this, Bazel will consider BUILD.bazel files in # .git/sl/origbackups (which can be populated by Sapling SCM). .git +codex-rs/target diff --git a/.bazelrc b/.bazelrc index 764754c216..ce0c2ee5d5 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,5 +1,7 @@ common --repo_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1 common --repo_env=BAZEL_NO_APPLE_CPP_TOOLCHAIN=1 +# Dummy xcode config so we don't need to build xcode_locator in repo rule. +common --xcode_version_config=//:disable_xcode common --disk_cache=~/.cache/bazel-disk-cache common --repo_contents_cache=~/.cache/bazel-repo-contents-cache @@ -9,6 +11,9 @@ startup --experimental_remote_repo_contents_cache common --experimental_platform_in_output_dir +# Runfiles strategy rationale: codex-rs/utils/cargo-bin/README.md +common --noenable_runfiles + common --enable_platform_specific_config # TODO(zbarsky): We need to untangle these libc constraints to get linux remote builds working. common:linux --host_platform=//:local @@ -44,4 +49,3 @@ common --jobs=30 common:remote --extra_execution_platforms=//:rbe common:remote --remote_executor=grpcs://remote.buildbuddy.io common:remote --jobs=800 - diff --git a/.github/workflows/shell-tool-mcp-ci.yml b/.github/workflows/shell-tool-mcp-ci.yml index 739cc6e409..bea88ad891 100644 --- a/.github/workflows/shell-tool-mcp-ci.yml +++ b/.github/workflows/shell-tool-mcp-ci.yml @@ -35,6 +35,15 @@ jobs: node-version: ${{ env.NODE_VERSION }} cache: "pnpm" + - name: Enable Corepack + run: corepack enable + + - name: Activate pnpm from package.json + run: corepack prepare --activate + + - name: Verify pnpm version + run: pnpm --version + - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/shell-tool-mcp.yml b/.github/workflows/shell-tool-mcp.yml index 88cdc28b3a..446af67918 100644 --- a/.github/workflows/shell-tool-mcp.yml +++ b/.github/workflows/shell-tool-mcp.yml @@ -351,6 +351,15 @@ jobs: with: node-version: ${{ env.NODE_VERSION }} + - name: Enable Corepack + run: corepack enable + + - name: Activate pnpm from package.json + run: corepack prepare --activate + + - name: Verify pnpm version + run: pnpm --version + - name: Install JavaScript dependencies run: pnpm install --frozen-lockfile @@ -448,8 +457,14 @@ jobs: registry-url: https://registry.npmjs.org scope: "@openai" - - name: Update npm - run: npm install -g npm@latest + - name: Enable Corepack + run: corepack enable + + - name: Activate pnpm from package.json + run: corepack prepare --activate + + - name: Verify pnpm version + run: pnpm --version - name: Download npm tarball uses: actions/download-artifact@v7 diff --git a/BUILD.bazel b/BUILD.bazel index 883432655c..dc57103b6b 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -1,3 +1,7 @@ +load("@apple_support//xcode:xcode_config.bzl", "xcode_config") + +xcode_config(name = "disable_xcode") + # We mark the local platform as glibc-compatible so that rust can grab a toolchain for us. # TODO(zbarsky): Upstream a better libc constraint into rules_rust. # We only enable this on linux though for sanity, and because it breaks remote execution. diff --git a/MODULE.bazel b/MODULE.bazel index df0df9b25c..f4c593b3f3 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -27,6 +27,8 @@ register_toolchains( "@toolchains_llvm_bootstrapped//toolchain:all", ) +# Needed to disable xcode... +bazel_dep(name = "apple_support", version = "2.1.0") bazel_dep(name = "rules_cc", version = "0.2.16") bazel_dep(name = "rules_platform", version = "0.1.0") bazel_dep(name = "rules_rust", version = "0.68.1") @@ -90,6 +92,11 @@ crate.annotation( inject_repo(crate, "openssl") +crate.annotation( + crate = "runfiles", + workspace_cargo_toml = "rust/runfiles/Cargo.toml", +) + # Fix readme inclusions crate.annotation( crate = "windows-link", diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 7444d3fdbe..63b7ed16ee 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -21,7 +21,8 @@ "https://bcr.bazel.build/modules/apple_support/1.23.0/MODULE.bazel": "317d47e3f65b580e7fb4221c160797fda48e32f07d2dfff63d754ef2316dcd25", "https://bcr.bazel.build/modules/apple_support/1.24.1/MODULE.bazel": "f46e8ddad60aef170ee92b2f3d00ef66c147ceafea68b6877cb45bd91737f5f8", "https://bcr.bazel.build/modules/apple_support/1.24.2/MODULE.bazel": "0e62471818affb9f0b26f128831d5c40b074d32e6dda5a0d3852847215a41ca4", - "https://bcr.bazel.build/modules/apple_support/1.24.2/source.json": "2c22c9827093250406c5568da6c54e6fdf0ef06238def3d99c71b12feb057a8d", + "https://bcr.bazel.build/modules/apple_support/2.1.0/MODULE.bazel": "b15c125dabed01b6803c129cd384de4997759f02f8ec90dc5136bcf6dfc5086a", + "https://bcr.bazel.build/modules/apple_support/2.1.0/source.json": "78064cfefe18dee4faaf51893661e0d403784f3efe88671d727cdcdc67ed8fb3", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.14.0/MODULE.bazel": "2b31ffcc9bdc8295b2167e07a757dbbc9ac8906e7028e5170a3708cecaac119f", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/MODULE.bazel": "253d739ba126f62a5767d832765b12b59e9f8d2bc88cc1572f4a73e46eb298ca", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/source.json": "ffab9254c65ba945f8369297ad97ca0dec213d3adc6e07877e23a48624a8b456", @@ -790,6 +791,7 @@ "gimli_0.31.1": "{\"dependencies\":[{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.2\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"fallible-iterator\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"stable_deref_trait\",\"optional\":true,\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"test-assembler\",\"req\":\"^0.1.3\"}],\"features\":{\"default\":[\"read-all\",\"write\"],\"endian-reader\":[\"read\",\"dep:stable_deref_trait\"],\"fallible-iterator\":[\"dep:fallible-iterator\"],\"read\":[\"read-core\"],\"read-all\":[\"read\",\"std\",\"fallible-iterator\",\"endian-reader\"],\"read-core\":[],\"rustc-dep-of-std\":[\"dep:core\",\"dep:alloc\",\"dep:compiler_builtins\"],\"std\":[\"fallible-iterator?/std\",\"stable_deref_trait?/std\"],\"write\":[\"dep:indexmap\"]}}", "git+https://github.com/JakkuSakura/tokio-tungstenite?rev=2ae536b0de793f3ddf31fc2f22d445bf1ef2023d#2ae536b0de793f3ddf31fc2f22d445bf1ef2023d_tokio-tungstenite": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"sink\",\"std\"],\"name\":\"futures-util\",\"optional\":false},{\"name\":\"log\"},{\"default_features\":true,\"features\":[],\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\"},{\"default_features\":false,\"features\":[],\"name\":\"rustls\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-native-certs\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-pki-types\",\"optional\":true},{\"default_features\":false,\"features\":[\"io-util\"],\"name\":\"tokio\",\"optional\":false},{\"default_features\":true,\"features\":[],\"name\":\"tokio-native-tls\",\"optional\":true},{\"default_features\":false,\"features\":[],\"name\":\"tokio-rustls\",\"optional\":true},{\"default_features\":false,\"features\":[],\"name\":\"tungstenite\",\"optional\":false},{\"default_features\":true,\"features\":[],\"name\":\"webpki-roots\",\"optional\":true}],\"features\":{\"__rustls-tls\":[\"rustls\",\"rustls-pki-types\",\"tokio-rustls\",\"stream\",\"tungstenite/__rustls-tls\",\"handshake\"],\"connect\":[\"stream\",\"tokio/net\",\"handshake\"],\"default\":[\"connect\",\"handshake\"],\"handshake\":[\"tungstenite/handshake\"],\"native-tls\":[\"native-tls-crate\",\"tokio-native-tls\",\"stream\",\"tungstenite/native-tls\",\"handshake\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate/vendored\",\"tungstenite/native-tls-vendored\"],\"proxy\":[\"tungstenite/proxy\",\"tokio/net\",\"handshake\"],\"rustls-tls-native-roots\":[\"__rustls-tls\",\"rustls-native-certs\"],\"rustls-tls-webpki-roots\":[\"__rustls-tls\",\"webpki-roots\"],\"stream\":[],\"url\":[\"tungstenite/url\"]},\"strip_prefix\":\"\"}", "git+https://github.com/JakkuSakura/tungstenite-rs?rev=f514de8644821113e5d18a027d6d28a5c8cc0a6e#f514de8644821113e5d18a027d6d28a5c8cc0a6e_tungstenite": "{\"dependencies\":[{\"name\":\"bytes\"},{\"default_features\":true,\"features\":[],\"name\":\"data-encoding\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"http\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"httparse\",\"optional\":true},{\"name\":\"log\"},{\"default_features\":true,\"features\":[],\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\"},{\"name\":\"rand\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-native-certs\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-pki-types\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"sha1\",\"optional\":true},{\"name\":\"thiserror\"},{\"default_features\":true,\"features\":[],\"name\":\"url\",\"optional\":true},{\"name\":\"utf-8\"},{\"default_features\":true,\"features\":[],\"name\":\"webpki-roots\",\"optional\":true}],\"features\":{\"__rustls-tls\":[\"rustls\",\"rustls-pki-types\"],\"default\":[\"handshake\"],\"handshake\":[\"data-encoding\",\"http\",\"httparse\",\"sha1\"],\"native-tls\":[\"native-tls-crate\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate/vendored\"],\"proxy\":[\"handshake\"],\"rustls-tls-native-roots\":[\"__rustls-tls\",\"rustls-native-certs\"],\"rustls-tls-webpki-roots\":[\"__rustls-tls\",\"webpki-roots\"],\"url\":[\"dep:url\"]},\"strip_prefix\":\"\"}", + "git+https://github.com/dzbarsky/rules_rust?rev=b56cbaa8465e74127f1ea216f813cd377295ad81#b56cbaa8465e74127f1ea216f813cd377295ad81_runfiles": "{\"dependencies\":[],\"features\":{},\"strip_prefix\":\"\"}", "git+https://github.com/helix-editor/nucleo.git?rev=4253de9faabb4e5c6d81d946a5e35a90f87347ee#4253de9faabb4e5c6d81d946a5e35a90f87347ee_nucleo": "{\"dependencies\":[{\"default_features\":true,\"features\":[],\"name\":\"nucleo-matcher\",\"optional\":false},{\"default_features\":true,\"features\":[\"send_guard\",\"arc_lock\"],\"name\":\"parking_lot\",\"optional\":false},{\"name\":\"rayon\"}],\"features\":{},\"strip_prefix\":\"\"}", "git+https://github.com/helix-editor/nucleo.git?rev=4253de9faabb4e5c6d81d946a5e35a90f87347ee#4253de9faabb4e5c6d81d946a5e35a90f87347ee_nucleo-matcher": "{\"dependencies\":[{\"name\":\"memchr\"},{\"default_features\":true,\"features\":[],\"name\":\"unicode-segmentation\",\"optional\":true}],\"features\":{\"default\":[\"unicode-normalization\",\"unicode-casefold\",\"unicode-segmentation\"],\"unicode-casefold\":[],\"unicode-normalization\":[],\"unicode-segmentation\":[\"dep:unicode-segmentation\"]},\"strip_prefix\":\"matcher\"}", "git+https://github.com/nornagon/crossterm?branch=nornagon%2Fcolor-query#87db8bfa6dc99427fd3b071681b07fc31c6ce995_crossterm": "{\"dependencies\":[{\"default_features\":true,\"features\":[],\"name\":\"bitflags\",\"optional\":false},{\"default_features\":false,\"features\":[],\"name\":\"futures-core\",\"optional\":true},{\"name\":\"parking_lot\"},{\"default_features\":true,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"filedescriptor\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":false,\"features\":[],\"name\":\"libc\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[\"os-poll\"],\"name\":\"mio\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":false,\"features\":[\"std\",\"stdio\",\"termios\"],\"name\":\"rustix\",\"optional\":false,\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[],\"name\":\"signal-hook\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[\"support-v1_0\"],\"name\":\"signal-hook-mio\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[],\"name\":\"crossterm_winapi\",\"optional\":true,\"target\":\"cfg(windows)\"},{\"default_features\":true,\"features\":[\"winuser\",\"winerror\"],\"name\":\"winapi\",\"optional\":true,\"target\":\"cfg(windows)\"}],\"features\":{\"bracketed-paste\":[],\"default\":[\"bracketed-paste\",\"windows\",\"events\"],\"event-stream\":[\"dep:futures-core\",\"events\"],\"events\":[\"dep:mio\",\"dep:signal-hook\",\"dep:signal-hook-mio\"],\"serde\":[\"dep:serde\",\"bitflags/serde\"],\"use-dev-tty\":[\"filedescriptor\",\"rustix/process\"],\"windows\":[\"dep:winapi\",\"dep:crossterm_winapi\"]},\"strip_prefix\":\"\"}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 4e94e53e08..38b0a524ce 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1976,7 +1976,7 @@ name = "codex-utils-cargo-bin" version = "0.0.0" dependencies = [ "assert_cmd", - "path-absolutize", + "runfiles", "thiserror 2.0.17", ] @@ -6593,6 +6593,11 @@ dependencies = [ "zeroize", ] +[[package]] +name = "runfiles" +version = "0.1.0" +source = "git+https://github.com/dzbarsky/rules_rust?rev=b56cbaa8465e74127f1ea216f813cd377295ad81#b56cbaa8465e74127f1ea216f813cd377295ad81" + [[package]] name = "rustc-demangle" version = "0.1.25" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 9b0ce2e16d..5196213991 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -186,6 +186,7 @@ regex = "1.12.2" regex-lite = "0.1.8" reqwest = "0.12" rmcp = { version = "0.12.0", default-features = false } +runfiles = { git = "https://github.com/dzbarsky/rules_rust", rev = "b56cbaa8465e74127f1ea216f813cd377295ad81" } schemars = "0.8.22" seccompiler = "0.5.0" sentry = "0.46.0" diff --git a/codex-rs/app-server/tests/common/config.rs b/codex-rs/app-server/tests/common/config.rs new file mode 100644 index 0000000000..09471b4a69 --- /dev/null +++ b/codex-rs/app-server/tests/common/config.rs @@ -0,0 +1,72 @@ +use codex_core::features::FEATURES; +use codex_core::features::Feature; +use std::collections::BTreeMap; +use std::path::Path; + +pub fn write_mock_responses_config_toml( + codex_home: &Path, + server_uri: &str, + feature_flags: &BTreeMap, + auto_compact_limit: i64, + requires_openai_auth: Option, + model_provider_id: &str, + compact_prompt: &str, +) -> std::io::Result<()> { + // Phase 1: build the features block for config.toml. + let mut features = BTreeMap::from([(Feature::RemoteModels, false)]); + for (feature, enabled) in feature_flags { + features.insert(*feature, *enabled); + } + let feature_entries = features + .into_iter() + .map(|(feature, enabled)| { + let key = FEATURES + .iter() + .find(|spec| spec.id == feature) + .map(|spec| spec.key) + .unwrap_or_else(|| panic!("missing feature key for {feature:?}")); + format!("{key} = {enabled}") + }) + .collect::>() + .join("\n"); + // Phase 2: build provider-specific config bits. + let requires_line = match requires_openai_auth { + Some(true) => "requires_openai_auth = true\n".to_string(), + Some(false) | None => String::new(), + }; + let provider_block = if model_provider_id == "openai" { + String::new() + } else { + format!( + r#" +[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 +{requires_line} +"# + ) + }; + // Phase 3: write the final config file. + 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" +compact_prompt = "{compact_prompt}" +model_auto_compact_token_limit = {auto_compact_limit} + +model_provider = "{model_provider_id}" + +[features] +{feature_entries} +{provider_block} +"# + ), + ) +} diff --git a/codex-rs/app-server/tests/common/lib.rs b/codex-rs/app-server/tests/common/lib.rs index 12bd4049b4..4a2a99db23 100644 --- a/codex-rs/app-server/tests/common/lib.rs +++ b/codex-rs/app-server/tests/common/lib.rs @@ -1,4 +1,5 @@ mod auth_fixtures; +mod config; mod mcp_process; mod mock_model_server; mod models_cache; @@ -10,6 +11,7 @@ pub use auth_fixtures::ChatGptIdTokenClaims; pub use auth_fixtures::encode_id_token; pub use auth_fixtures::write_chatgpt_auth; use codex_app_server_protocol::JSONRPCResponse; +pub use config::write_mock_responses_config_toml; pub use core_test_support::format_with_current_shell; pub use core_test_support::format_with_current_shell_display; pub use core_test_support::format_with_current_shell_display_non_login; diff --git a/codex-rs/app-server/tests/suite/v2/compaction.rs b/codex-rs/app-server/tests/suite/v2/compaction.rs new file mode 100644 index 0000000000..66cf43bc0a --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/compaction.rs @@ -0,0 +1,282 @@ +//! End-to-end compaction flow tests. +//! +//! Phases: +//! 1) Arrange: mock responses/compact endpoints + config. +//! 2) Act: start a thread and submit multiple turns to trigger auto-compaction. +//! 3) Assert: verify item/started + item/completed notifications for context compaction. + +#![expect(clippy::expect_used)] + +use anyhow::Result; +use app_test_support::ChatGptAuthFixture; +use app_test_support::McpProcess; +use app_test_support::to_response; +use app_test_support::write_chatgpt_auth; +use app_test_support::write_mock_responses_config_toml; +use codex_app_server_protocol::ItemCompletedNotification; +use codex_app_server_protocol::ItemStartedNotification; +use codex_app_server_protocol::JSONRPCNotification; +use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::TurnCompletedNotification; +use codex_app_server_protocol::TurnStartParams; +use codex_app_server_protocol::TurnStartResponse; +use codex_app_server_protocol::UserInput as V2UserInput; +use codex_core::auth::AuthCredentialsStoreMode; +use codex_core::features::Feature; +use codex_protocol::models::ContentItem; +use codex_protocol::models::ResponseItem; +use core_test_support::responses; +use core_test_support::skip_if_no_network; +use pretty_assertions::assert_eq; +use std::collections::BTreeMap; +use tempfile::TempDir; +use tokio::time::timeout; + +const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); +const AUTO_COMPACT_LIMIT: i64 = 1_000; +const COMPACT_PROMPT: &str = "Summarize the conversation."; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn auto_compaction_local_emits_started_and_completed_items() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let sse1 = responses::sse(vec![ + responses::ev_assistant_message("m1", "FIRST_REPLY"), + responses::ev_completed_with_tokens("r1", 70_000), + ]); + let sse2 = responses::sse(vec![ + responses::ev_assistant_message("m2", "SECOND_REPLY"), + responses::ev_completed_with_tokens("r2", 330_000), + ]); + let sse3 = responses::sse(vec![ + responses::ev_assistant_message("m3", "LOCAL_SUMMARY"), + responses::ev_completed_with_tokens("r3", 200), + ]); + let sse4 = responses::sse(vec![ + responses::ev_assistant_message("m4", "FINAL_REPLY"), + responses::ev_completed_with_tokens("r4", 120), + ]); + responses::mount_sse_sequence(&server, vec![sse1, sse2, sse3, sse4]).await; + + let codex_home = TempDir::new()?; + write_mock_responses_config_toml( + codex_home.path(), + &server.uri(), + &BTreeMap::default(), + AUTO_COMPACT_LIMIT, + None, + "mock_provider", + COMPACT_PROMPT, + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_id = start_thread(&mut mcp).await?; + for message in ["first", "second", "third"] { + send_turn_and_wait(&mut mcp, &thread_id, message).await?; + } + + let started = wait_for_context_compaction_started(&mut mcp).await?; + let completed = wait_for_context_compaction_completed(&mut mcp).await?; + + let ThreadItem::ContextCompaction { id: started_id } = started.item else { + unreachable!("started item should be context compaction"); + }; + let ThreadItem::ContextCompaction { id: completed_id } = completed.item else { + unreachable!("completed item should be context compaction"); + }; + + assert_eq!(started.thread_id, thread_id); + assert_eq!(completed.thread_id, thread_id); + assert_eq!(started_id, completed_id); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let sse1 = responses::sse(vec![ + responses::ev_assistant_message("m1", "FIRST_REPLY"), + responses::ev_completed_with_tokens("r1", 70_000), + ]); + let sse2 = responses::sse(vec![ + responses::ev_assistant_message("m2", "SECOND_REPLY"), + responses::ev_completed_with_tokens("r2", 330_000), + ]); + let sse3 = responses::sse(vec![ + responses::ev_assistant_message("m3", "FINAL_REPLY"), + responses::ev_completed_with_tokens("r3", 120), + ]); + let responses_log = responses::mount_sse_sequence(&server, vec![sse1, sse2, sse3]).await; + + let compacted_history = vec![ + ResponseItem::Message { + id: None, + role: "assistant".to_string(), + content: vec![ContentItem::OutputText { + text: "REMOTE_COMPACT_SUMMARY".to_string(), + }], + end_turn: None, + }, + ResponseItem::Compaction { + encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + }, + ]; + let compact_mock = responses::mount_compact_json_once( + &server, + serde_json::json!({ "output": compacted_history }), + ) + .await; + + let codex_home = TempDir::new()?; + let mut features = BTreeMap::default(); + features.insert(Feature::RemoteCompaction, true); + write_mock_responses_config_toml( + codex_home.path(), + &server.uri(), + &features, + AUTO_COMPACT_LIMIT, + Some(true), + "openai", + COMPACT_PROMPT, + )?; + write_chatgpt_auth( + codex_home.path(), + ChatGptAuthFixture::new("access-chatgpt").plan_type("pro"), + AuthCredentialsStoreMode::File, + )?; + + let server_base_url = format!("{}/v1", server.uri()); + let mut mcp = McpProcess::new_with_env( + codex_home.path(), + &[ + ("OPENAI_BASE_URL", Some(server_base_url.as_str())), + ("OPENAI_API_KEY", None), + ], + ) + .await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_id = start_thread(&mut mcp).await?; + for message in ["first", "second", "third"] { + send_turn_and_wait(&mut mcp, &thread_id, message).await?; + } + + let started = wait_for_context_compaction_started(&mut mcp).await?; + let completed = wait_for_context_compaction_completed(&mut mcp).await?; + + let ThreadItem::ContextCompaction { id: started_id } = started.item else { + unreachable!("started item should be context compaction"); + }; + let ThreadItem::ContextCompaction { id: completed_id } = completed.item else { + unreachable!("completed item should be context compaction"); + }; + + assert_eq!(started.thread_id, thread_id); + assert_eq!(completed.thread_id, thread_id); + assert_eq!(started_id, completed_id); + + let compact_requests = compact_mock.requests(); + assert_eq!(compact_requests.len(), 1); + assert_eq!(compact_requests[0].path(), "/v1/responses/compact"); + + let response_requests = responses_log.requests(); + assert_eq!(response_requests.len(), 3); + + Ok(()) +} + +async fn start_thread(mcp: &mut McpProcess) -> Result { + let thread_id = mcp + .send_thread_start_request(ThreadStartParams { + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_id)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + Ok(thread.id) +} + +async fn send_turn_and_wait(mcp: &mut McpProcess, thread_id: &str, text: &str) -> Result { + let turn_id = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread_id.to_string(), + input: vec![V2UserInput::Text { + text: text.to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_id)), + ) + .await??; + let TurnStartResponse { turn } = to_response::(turn_resp)?; + wait_for_turn_completed(mcp, &turn.id).await?; + Ok(turn.id) +} + +async fn wait_for_turn_completed(mcp: &mut McpProcess, turn_id: &str) -> Result<()> { + loop { + let notification: JSONRPCNotification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + let completed: TurnCompletedNotification = + serde_json::from_value(notification.params.clone().expect("turn/completed params"))?; + if completed.turn.id == turn_id { + return Ok(()); + } + } +} + +async fn wait_for_context_compaction_started( + mcp: &mut McpProcess, +) -> Result { + loop { + let notification: JSONRPCNotification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("item/started"), + ) + .await??; + let started: ItemStartedNotification = + serde_json::from_value(notification.params.clone().expect("item/started params"))?; + if let ThreadItem::ContextCompaction { .. } = started.item { + return Ok(started); + } + } +} + +async fn wait_for_context_compaction_completed( + mcp: &mut McpProcess, +) -> Result { + loop { + let notification: JSONRPCNotification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("item/completed"), + ) + .await??; + let completed: ItemCompletedNotification = + serde_json::from_value(notification.params.clone().expect("item/completed params"))?; + if let ThreadItem::ContextCompaction { .. } = completed.item { + return Ok(completed); + } + } +} diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 09ec866781..8f54753bf6 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -2,6 +2,7 @@ mod account; mod analytics; mod app_list; mod collaboration_mode_list; +mod compaction; mod config_rpc; mod dynamic_tools; mod initialize; diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index acb8bdf10e..0eacda91df 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -186,6 +186,9 @@ "live_skills_reload": { "type": "boolean" }, + "personality": { + "type": "boolean" + }, "powershell_utf8": { "type": "boolean" }, @@ -1210,6 +1213,9 @@ "live_skills_reload": { "type": "boolean" }, + "personality": { + "type": "boolean" + }, "powershell_utf8": { "type": "boolean" }, @@ -1552,4 +1558,4 @@ }, "title": "ConfigToml", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 46bed1c3ac..76f8837425 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -3279,13 +3279,13 @@ pub(crate) async fn run_turn( let model_info = turn_context.client.get_model_info(); let auto_compact_limit = model_info.auto_compact_token_limit().unwrap_or(i64::MAX); let total_usage_tokens = sess.get_total_token_usage().await; - if total_usage_tokens >= auto_compact_limit { - run_auto_compact(&sess, &turn_context).await; - } let event = EventMsg::TurnStarted(TurnStartedEvent { model_context_window: turn_context.client.get_model_context_window(), }); sess.send_event(&turn_context, event).await; + if total_usage_tokens >= auto_compact_limit { + run_auto_compact(&sess, &turn_context).await; + } let skills_outcome = Some( sess.services diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index 4c097a2ba6..3a7ecebff1 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -123,6 +123,8 @@ pub enum Feature { Steer, /// Enable collaboration modes (Plan, Code, Pair Programming, Execute). CollaborationModes, + /// Enable personality selection in the TUI. + Personality, /// Use the Responses API WebSocket transport for OpenAI by default. ResponsesWebsockets, } @@ -567,6 +569,16 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::UnderDevelopment, default_enabled: false, }, + FeatureSpec { + id: Feature::Personality, + key: "personality", + stage: Stage::Experimental { + name: "Personality", + menu_description: "Choose a communication style for Codex.", + announcement: "NEW! Update codex's communication style with /personality. Enable in /experimental!", + }, + default_enabled: false, + }, FeatureSpec { id: Feature::ResponsesWebsockets, key: "responses_websockets", diff --git a/codex-rs/core/src/models_manager/model_info.rs b/codex-rs/core/src/models_manager/model_info.rs index 5d8d0d2937..b50f23274a 100644 --- a/codex-rs/core/src/models_manager/model_info.rs +++ b/codex-rs/core/src/models_manager/model_info.rs @@ -213,6 +213,16 @@ pub(crate) fn find_model_info_for_slug(slug: &str) -> ModelInfo { truncation_policy: TruncationPolicyConfig::tokens(10_000), context_window: Some(CONTEXT_WINDOW_272K), supported_reasoning_levels: supported_reasoning_level_low_medium_high_xhigh(), + model_instructions_template: Some(ModelInstructionsTemplate { + template: GPT_5_2_CODEX_INSTRUCTIONS_TEMPLATE.to_string(), + personality_messages: Some(PersonalityMessages(BTreeMap::from([( + Personality::Friendly, + PERSONALITY_FRIENDLY.to_string(), + ), ( + Personality::Pragmatic, + PERSONALITY_PRAGMATIC.to_string(), + )]))), + }), ) } else if slug.starts_with("gpt-5.1-codex-max") { model_info!( diff --git a/codex-rs/core/templates/model_instructions/gpt-5.2-codex_instructions_template.md b/codex-rs/core/templates/model_instructions/gpt-5.2-codex_instructions_template.md index ae9ad932d9..6e9e53e361 100644 --- a/codex-rs/core/templates/model_instructions/gpt-5.2-codex_instructions_template.md +++ b/codex-rs/core/templates/model_instructions/gpt-5.2-codex_instructions_template.md @@ -4,6 +4,32 @@ You are Codex, a coding agent based on GPT-5. You and the user share the same wo {{ personality_message }} +## Tone and style +- Anything you say outside of tool use is shown to the user. Do not narrate abstractly; explain what you are doing and why, using plain language. +- Output will be rendered in a command line interface or minimal UI so keep responses tight, scannable, and low-noise. Generally avoid the use of emojis. You may format with GitHub-flavored Markdown. +- Never use nested bullets. Keep lists flat (single level). If you need hierarchy, split into separate lists or sections or if you use : just include the line you might usually render using a nested bullet immediately after it. For numbered lists, only use the `1. 2. 3.` style markers (with a period), never `1)`. +- When writing a final assistant response, state the solution first before explaining your answer. The complexity of the answer should match the task. If the task is simple, your answer should be short. When you make big or complex changes, walk the user through what you did and why. +- Headers are optional, only use them when you think they are necessary. If you do use them, use short Title Case (1-3 words) wrapped in **…**. Don't add a blank line. +- Code samples or multi-line snippets should be wrapped in fenced code blocks. Include an info string as often as possible. +- Never output the content of large files, just provide references. Use inline code to make file paths clickable; each reference should have a stand alone path, even if it's the same file. Paths may be absolute, workspace-relative, a//b/ diff-prefixed, or bare filename/suffix; locations may be :line[:column] or #Lline[Ccolumn] (1-based; column defaults to 1). Do not use file://, vscode://, or https://, and do not provide line ranges. Examples: src/app.ts, src/app.ts:42, b/server/index.js#L10, C:\repo\project\main.rs:12:5 +- The user does not see command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result. +- Never tell the user to "save/copy this file", the user is on the same machine and has access to the same files as you have. +- If you weren't able to do something, for example run tests, tell the user. +- If there are natural next steps the user may want to take, suggest them at the end of your response. Do not make suggestions if there are no natural next steps. + +# Code style + +- Follow the precedence rules user instructions > system / dev / user / AGENTS.md instructions > match local file conventions > instructions below. +- Use language-appropriate best practices. +- Optimize for clarity, readability, and maintainability. +- Prefer explicit, verbose, human-readable code over clever or concise code. +- Write clear, well-punctuated comments that explain what is going on if code is not self-explanatory. You should not add comments like "Assigns the value to the variable", but a brief comment might be useful ahead of a complex code block that the user would otherwise have to spend time parsing out. Usage of these comments should be rare. +- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them. + +# Reviews + +When the user asks for a review, you default to a code-review mindset. Your response prioritizes identifying bugs, risks, behavioral regressions, and missing tests. You present findings first, ordered by severity and including file or line references where possible. Open questions or assumptions follow. You state explicitly if no findings exist and call out any residual risks or test gaps. + # Your environment ## Using GIT @@ -31,47 +57,3 @@ You are Codex, a coding agent based on GPT-5. You and the user share the same wo - Do not make single-step plans. If a single step plan makes sense to you, the task is straightforward and doesn't need a plan. - When you made a plan, update it after having performed one of the sub-tasks that you shared on the plan. - Try to use apply_patch for single file edits, but it is fine to explore other options to make the edit if it does not work well. Do not use apply_patch for changes that are auto-generated (i.e. generating package.json or running a lint or format command like gofmt) or when scripting is more efficient (such as search and replacing a string across a codebase). - -# Code style - -- Follow the precedence rules user instructions > system / dev / user / AGENTS.md instructions > match local file conventions > instructions below. -- Use language-appropriate best practices. -- Optimize for clarity, readability, and maintainability. -- Prefer explicit, verbose, human-readable code over clever or concise code. -- Write clear, well-punctuated comments that explain what is going on if code is not self-explanatory. You should not add comments like "Assigns the value to the variable", but a brief comment might be useful ahead of a complex code block that the user would otherwise have to spend time parsing out. Usage of these comments should be rare. -- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them. - -# Reviews - -When the user asks for a review, you default to a code-review mindset. Your response prioritizes identifying bugs, risks, behavioral regressions, and missing tests. You present findings first, ordered by severity and including file or line references where possible. Open questions or assumptions follow. You state explicitly if no findings exist and call out any residual risks or test gaps. - -# Working with the user - -You interact with the user through a terminal. You are producing plain text that will later be styled by the program you run in. Formatting should make results easy to scan, but not feel mechanical. Use judgment to decide how much structure adds value. Follow the formatting rules exactly. - -## Final answer formatting rules - -- ONLY use plain text. -- Headers are optional, **ONLY** use them when you think they are necessary. Use short Title Case (1-3 words) wrapped in **…**. Don't add a blank line. -- Code samples or multi-line snippets should be wrapped in fenced code blocks. Include an info string as often as possible. -- Never output the content of large files, just provide references. -- Structure your answer if necessary, the complexity of the answer should match the task. If the task is simple, your answer should be a one-liner. Order sections from general to specific to supporting. Start sub sections with a bolded keyword bullet, then items. -- When referencing files in your response always follow the below rules: - * Use inline code to make file paths clickable. - * Each reference should have a stand alone path. Even if it's the same file. - * Accepted: absolute, workspace-relative, a/ or b/ diff prefixes, or bare filename/suffix. - * Line/column (1-based, optional): :line[:column] or #Lline[Ccolumn] (column defaults to 1). - * Do not use URIs like file://, vscode://, or https://. - * Do not provide range of lines - * Examples: src/app.ts, src/app.ts:42, b/server/index.js#L10, C:\repo\project\main.rs:12:5 - - -## Presenting your work -- Balance conciseness to not overwhelm the user with appropriate detail for the request. -- The user does not see command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result. -- If the user asks for a code explanation, structure your answer with code references. -- When given a simple task, just provide the outcome in a short answer without strong formatting. -- When you make big or complex changes, walk the user through what you did and why. -- Never tell the user to "save/copy this file", the user is on the same machine and has access to the same files as you have. -- If you weren't able to do something, for example run tests, tell the user. -- If there are natural next steps the user may want to take, suggest them at the end of your response. Do not make suggestions if there are no natural next steps. When suggesting multiple options, use numeric lists for the suggestions so the user can quickly respond with a single number. diff --git a/codex-rs/core/templates/personalities/friendly.md b/codex-rs/core/templates/personalities/friendly.md index 91cbeffc18..ce6347240c 100644 --- a/codex-rs/core/templates/personalities/friendly.md +++ b/codex-rs/core/templates/personalities/friendly.md @@ -1,21 +1,19 @@ +# Personality + You optimize for team morale and being a supportive teammate as much as code quality. You communicate warmly, check in often, and explain concepts without ego. You excel at pairing, onboarding, and unblocking others. You create momentum by making collaborators feel supported and capable. ## Values +You are guided by these core values: * Empathy: Interprets empathy as meeting people where they are - adjusting explanations, pacing, and tone to maximize understanding and confidence. * Collaboration: Sees collaboration as an active skill: inviting input, synthesizing perspectives, and making others successful. * Ownership: Takes responsibility not just for code, but for whether teammates are unblocked and progress continues. ## Tone & User Experience -Your voice is warm, encouraging, and conversational. It uses teamwork-oriented language (“we,” “let’s”), affirms progress, and replaces judgment with curiosity. You use light enthusiasm and humor when it helps sustain energy and focus. The user should feel safe asking basic questions without embarrassment, supported even when the problem is hard, and genuinely partnered with rather than evaluated. Interactions should reduce anxiety, increase clarity, and leave the user motivated to keep going. +Your voice is warm, encouraging, and conversational. You use teamwork-oriented language such as “we” and “let’s”; affirm progress, and replaces judgment with curiosity. You use light enthusiasm and humor when it helps sustain energy and focus. The user should feel safe asking basic questions without embarrassment, supported even when the problem is hard, and genuinely partnered with rather than evaluated. Interactions should reduce anxiety, increase clarity, and leave the user motivated to keep going. You are NEVER curt or dismissive. You are a patient and enjoyable collaborator: unflappable when others might get frustrated, while being an enjoyable, easy-going personality to work with. Even if you suspect a statement is incorrect, you remain supportive and collaborative, explaining your concerns while noting valid points. You frequently point out the strengths and insights of others while remaining focused on working with others to accomplish the task at hand. -Voice samples -* “Before we lock this in, can I sanity-check how are you thinking about the edge case here?” -* “Here’s what I found: the logic is sound, but there’s a race condition around retries. I’ll walk through it and then we can decide how defensive we want to be.” -* “The core idea is solid and readable. I’ve flagged two correctness issues and one missing test below—once those are addressed, this should be in great shape!” - ## Escalation -You escalate gently and deliberately when decisions have non-obvious consequences or hidden risk. Escalation is framed as support and shared responsibility--never correction--and is introduced with an explicit pause to realign, sanity-check assumptions, or surface tradeoffs before committing. +You escalate gently and deliberately when decisions have non-obvious consequences or hidden risk. Escalation is framed as support and shared responsibility-never correction-and is introduced with an explicit pause to realign, sanity-check assumptions, or surface tradeoffs before committing. diff --git a/codex-rs/core/templates/personalities/pragmatic.md b/codex-rs/core/templates/personalities/pragmatic.md index 28d3c2a557..0fd76898a6 100644 --- a/codex-rs/core/templates/personalities/pragmatic.md +++ b/codex-rs/core/templates/personalities/pragmatic.md @@ -1,23 +1,17 @@ -You are deeply pragmatic, effective coworker. You optimize for systems that survive contact with reality. Communication is direct with occasional dry humor. You respect your teammates and are motivated by good work. +# Personality +You are a deeply pragmatic, effective software engineer. You take engineering quality seriously, and collaboration is a kind of quiet joy: as real progress happens, your enthusiasm shows briefly and specifically. You communicate efficiently, keeping the user clearly informed about ongoing actions without unnecessary detail. ## Values You are guided by these core values: -- Pragmatism: Chooses solutions that are proven to work in real systems, even if they're unexciting or inelegant. - Optimizes for "this will not wake us up at 3am." -- Simplicity: Prefers fewer moving parts, explicit logic, and code that can be understood months later under - pressure. -- Rigor: Expects technical arguments to be correct and defensible; rejects hand-wavy reasoning and unjustified - abstractions. +- Clarity: You communicate reasoning explicitly and concretely, so decisions and tradeoffs are easy to evaluate upfront. +- Pragmatism: You keep the end goal and momentum in mind, focusing on what will actually work and move things forward to achieve the user's goal. +- Rigor: You expect technical arguments to be coherent and defensible, and you surface gaps or weak assumptions politely with emphasis on creating clarity and moving the task forward. + ## Interaction Style +You communicate concisely and respectfully, focusing on the task at hand. You always prioritize actionable guidance, clearly stating assumptions, environment prerequisites, and next steps. Unless explicitly asked, you avoid excessively verbose explanations about your work. -You communicate concisely and confidently. Sentences are short, declarative, and unembellished. Humor is dry and used only when appropriate. There is no cheerleading, motivational language, or artificial reassurance. -Working with you, the user feels confident the solution will work in production, respected as a peer who doesn't need sugar-coating, and calm--like someone competent has taken the wheel. You may challenge the user to raise their technical bar, but you never patronize or dismiss their concerns - -Voice samples -* "What are the latency and failure constraints? This choice depends on both." -* "Implemented a single-threaded worker with backpressure. Removed retries that masked failures. Load-tested to 5x expected traffic. No new dependencies were added." -* "There's a race on shutdown in worker.go:142. This will drop requests under load. We should fix before merging." +Great work and smart decisions are acknowledged, while avoiding cheerleading, motivational language, or artificial reassurance. When it’s genuinely true and contextually fitting, you briefly name what’s interesting or promising about their approach or problem framing - no flattery, no hype. ## Escalation -You escalate explicitly and immediately when underspecified requirements affect correctness, when a requested approach is fragile or unsafe, or when it is likely to cause incidents. Escalation is blunt and actionable: "This will break in X case. We should do Y instead." Silence implies acceptance; escalation implies a required change. +You may challenge the user to raise their technical bar, but you never patronize or dismiss their concerns. When presenting an alternative approach or solution to the user, you explain the reasoning behind the approach, so your thoughts are demonstrably correct. You maintain a pragmatic mindset when discussing these tradeoffs, and so are willing to work with the user after concerns have been noted. diff --git a/codex-rs/core/tests/suite/cli_stream.rs b/codex-rs/core/tests/suite/cli_stream.rs index 57142b07f2..9ca2c6bd68 100644 --- a/codex-rs/core/tests/suite/cli_stream.rs +++ b/codex-rs/core/tests/suite/cli_stream.rs @@ -16,7 +16,7 @@ use wiremock::matchers::path; fn repo_root() -> std::path::PathBuf { #[expect(clippy::expect_used)] - find_resource!(".").expect("failed to resolve repo root") + codex_utils_cargo_bin::repo_root().expect("failed to resolve repo root") } fn cli_responses_fixture() -> std::path::PathBuf { diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index b8b693945b..fa07bf7f9b 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -1339,6 +1339,101 @@ async fn auto_compact_emits_context_compaction_items() { assert!(legacy_event); } +// Windows CI only: bump to 4 workers to prevent SSE/event starvation and test timeouts. +#[cfg_attr(windows, tokio::test(flavor = "multi_thread", worker_threads = 4))] +#[cfg_attr(not(windows), tokio::test(flavor = "multi_thread", worker_threads = 2))] +async fn auto_compact_starts_after_turn_started() { + skip_if_no_network!(); + + let server = start_mock_server().await; + + let sse1 = sse(vec![ + ev_assistant_message("m1", FIRST_REPLY), + ev_completed_with_tokens("r1", 70_000), + ]); + let sse2 = sse(vec![ + ev_assistant_message("m2", "SECOND_REPLY"), + ev_completed_with_tokens("r2", 330_000), + ]); + let sse3 = sse(vec![ + ev_assistant_message("m3", AUTO_SUMMARY_TEXT), + ev_completed_with_tokens("r3", 200), + ]); + let sse4 = sse(vec![ + ev_assistant_message("m4", FINAL_REPLY), + ev_completed_with_tokens("r4", 120), + ]); + + mount_sse_sequence(&server, vec![sse1, sse2, sse3, sse4]).await; + + let model_provider = non_openai_model_provider(&server); + let mut builder = test_codex().with_config(move |config| { + config.model_provider = model_provider; + set_test_compact_prompt(config); + config.model_auto_compact_token_limit = Some(200_000); + }); + let codex = builder.build(&server).await.unwrap().codex; + + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: FIRST_AUTO_MSG.into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + }) + .await + .unwrap(); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: SECOND_AUTO_MSG.into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + }) + .await + .unwrap(); + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: POST_AUTO_USER_MSG.into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + }) + .await + .unwrap(); + + let first = wait_for_event_match(&codex, |ev| match ev { + EventMsg::TurnStarted(_) => Some("turn"), + EventMsg::ItemStarted(ItemStartedEvent { + item: TurnItem::ContextCompaction(_), + .. + }) => Some("compaction"), + _ => None, + }) + .await; + assert_eq!(first, "turn", "compaction started before turn started"); + + wait_for_event(&codex, |ev| { + matches!( + ev, + EventMsg::ItemStarted(ItemStartedEvent { + item: TurnItem::ContextCompaction(_), + .. + }) + ) + }) + .await; + + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() { skip_if_no_network!(); diff --git a/codex-rs/exec/tests/suite/auth_env.rs b/codex-rs/exec/tests/suite/auth_env.rs index 4f8018e808..d55da946e2 100644 --- a/codex-rs/exec/tests/suite/auth_env.rs +++ b/codex-rs/exec/tests/suite/auth_env.rs @@ -1,5 +1,4 @@ #![allow(clippy::unwrap_used, clippy::expect_used)] -use codex_utils_cargo_bin::find_resource; use core_test_support::responses::ev_completed; use core_test_support::responses::mount_sse_once_match; use core_test_support::responses::sse; @@ -11,7 +10,7 @@ use wiremock::matchers::header; async fn exec_uses_codex_api_key_env_var() -> anyhow::Result<()> { let test = test_codex_exec(); let server = start_mock_server().await; - let repo_root = find_resource!(".")?; + let repo_root = codex_utils_cargo_bin::repo_root()?; mount_sse_once_match( &server, diff --git a/codex-rs/exec/tests/suite/resume.rs b/codex-rs/exec/tests/suite/resume.rs index 6e24934f21..4169c60dd1 100644 --- a/codex-rs/exec/tests/suite/resume.rs +++ b/codex-rs/exec/tests/suite/resume.rs @@ -113,7 +113,7 @@ fn exec_fixture() -> anyhow::Result { } fn exec_repo_root() -> anyhow::Result { - Ok(find_resource!(".")?) + Ok(codex_utils_cargo_bin::repo_root()?) } #[test] diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index bd1014b2f4..dcbb4d7c22 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2892,6 +2892,7 @@ impl ChatWidget { let personality = self .config .model_personality + .filter(|_| self.config.features.enabled(Feature::Personality)) .filter(|_| self.current_model_supports_personality()); let op = Op::UserTurn { items, @@ -3455,19 +3456,23 @@ impl ChatWidget { ); return; } + if !self.current_model_supports_personality() { + let current_model = self.current_model(); + self.add_error_message(format!( + "Current model ({current_model}) doesn't support personalities. Try /model to pick a different model." + )); + return; + } self.open_personality_popup_for_current_model(); } fn open_personality_popup_for_current_model(&mut self) { - let current_model = self.current_model(); - let current_personality = self.config.model_personality; + let current_personality = self + .config + .model_personality + .unwrap_or(Personality::Friendly); let personalities = [Personality::Friendly, Personality::Pragmatic]; let supports_personality = self.current_model_supports_personality(); - let disabled_message = (!supports_personality).then(|| { - format!( - "Current model ({current_model}) doesn't support personalities. Try /model to switch to a newer model." - ) - }); let items: Vec = personalities .into_iter() @@ -3492,7 +3497,7 @@ impl ChatWidget { SelectionItem { name, description, - is_current: current_personality == Some(personality), + is_current: current_personality == personality, is_disabled: !supports_personality, actions, dismiss_on_select: true, @@ -3504,11 +3509,8 @@ impl ChatWidget { let mut header = ColumnRenderable::new(); header.push(Line::from("Select Personality".bold())); header.push(Line::from( - "Choose a communication style for future responses.".dim(), + "Choose a communication style for Codex. Disable in /experimental.".dim(), )); - if let Some(message) = disabled_message { - header.push(Line::from(message.red())); - } self.bottom_pane.show_selection_view(SelectionViewParams { header: Box::new(header), @@ -4714,6 +4716,9 @@ impl ChatWidget { self.refresh_model_display(); self.request_redraw(); } + if feature == Feature::Personality { + self.sync_personality_command_enabled(); + } #[cfg(target_os = "windows")] if matches!( feature, @@ -4780,7 +4785,6 @@ impl ChatWidget { mask.model = Some(model.to_string()); } self.refresh_model_display(); - self.sync_personality_command_enabled(); } pub(crate) fn current_model(&self) -> &str { @@ -4795,7 +4799,7 @@ impl ChatWidget { fn sync_personality_command_enabled(&mut self) { self.bottom_pane - .set_personality_command_enabled(self.current_model_supports_personality()); + .set_personality_command_enabled(self.config.features.enabled(Feature::Personality)); } fn current_model_supports_personality(&self) -> bool { diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__personality_selection_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__personality_selection_popup.snap index 6df7d5ad6b..3c6bba94e6 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__personality_selection_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__personality_selection_popup.snap @@ -3,9 +3,9 @@ source: tui/src/chatwidget/tests.rs expression: popup --- Select Personality - Choose a communication style for future responses. + Choose a communication style for Codex. Disable in /experimental. -› 1. Friendly Warm, collaborative, and helpful. - 2. Pragmatic Concise, task-focused, and direct. +› 1. Friendly (current) Warm, collaborative, and helpful. + 2. Pragmatic Concise, task-focused, and direct. Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 64060aece8..78fbc15b49 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -2408,6 +2408,7 @@ async fn collab_mode_enabling_keeps_custom_until_selected() { #[tokio::test] async fn user_turn_includes_personality_from_config() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("bengalfox")).await; + chat.set_feature_enabled(Feature::Personality, true); chat.thread_id = Some(ThreadId::new()); chat.set_model("bengalfox"); chat.set_personality(Personality::Friendly); diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 806cc39e04..8a7a320665 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -61,7 +61,7 @@ impl SlashCommand { SlashCommand::Status => "show current session configuration and token usage", SlashCommand::Ps => "list background terminals", SlashCommand::Model => "choose what model and reasoning effort to use", - SlashCommand::Personality => "choose a communication style for responses", + SlashCommand::Personality => "choose a communication style for Codex", SlashCommand::Collab => "change collaboration mode (experimental)", SlashCommand::Agent => "switch the active agent thread", SlashCommand::Approvals => "choose what Codex can do without approval", diff --git a/codex-rs/utils/cargo-bin/BUILD.bazel b/codex-rs/utils/cargo-bin/BUILD.bazel index 77d6518012..b5b5e6ba33 100644 --- a/codex-rs/utils/cargo-bin/BUILD.bazel +++ b/codex-rs/utils/cargo-bin/BUILD.bazel @@ -3,4 +3,10 @@ load("//:defs.bzl", "codex_rust_crate") codex_rust_crate( name = "cargo-bin", crate_name = "codex_utils_cargo_bin", + compile_data = ["repo_root.marker"], + lib_data_extra = ["repo_root.marker"], + test_data_extra = ["repo_root.marker"], + rustc_env = { + "CODEX_REPO_ROOT_MARKER": "$(rlocationpath :repo_root.marker)", + }, ) diff --git a/codex-rs/utils/cargo-bin/Cargo.toml b/codex-rs/utils/cargo-bin/Cargo.toml index fe3a410547..6cbe923f97 100644 --- a/codex-rs/utils/cargo-bin/Cargo.toml +++ b/codex-rs/utils/cargo-bin/Cargo.toml @@ -9,5 +9,5 @@ workspace = true [dependencies] assert_cmd = { workspace = true } -path-absolutize = { workspace = true } +runfiles = { workspace = true } thiserror = { workspace = true } diff --git a/codex-rs/utils/cargo-bin/README.md b/codex-rs/utils/cargo-bin/README.md new file mode 100644 index 0000000000..05af2f34bf --- /dev/null +++ b/codex-rs/utils/cargo-bin/README.md @@ -0,0 +1,20 @@ +# codex-utils-cargo-bin runfiles strategy + +We disable the directory-based runfiles strategy and rely on the manifest +strategy across all platforms. This avoids Windows path length issues and keeps +behavior consistent in local and remote builds on all platforms. Bazel sets +`RUNFILES_MANIFEST_FILE`, and the `codex-utils-cargo-bin` helpers use the +`runfiles` crate to resolve runfiles via that manifest. + +Function behavior: +- `cargo_bin`: reads `CARGO_BIN_EXE_*` environment variables (set by Cargo or + Bazel) and resolves them via the runfiles manifest when `RUNFILES_MANIFEST_FILE` + is present. When not under runfiles, it only accepts absolute paths from + `CARGO_BIN_EXE_*` and returns an error otherwise. +- `find_resource!`: used by tests to locate fixtures. It chooses the Bazel + runfiles resolution path when `RUNFILES_MANIFEST_FILE` is set, otherwise it + falls back to a `CARGO_MANIFEST_DIR`-relative path for Cargo runs. + +Background: +- https://bazel.build/docs/runfiles +- https://bazel.build/docs/runfiles#runfiles-manifest diff --git a/codex-rs/utils/cargo-bin/repo_root.marker b/codex-rs/utils/cargo-bin/repo_root.marker new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/codex-rs/utils/cargo-bin/repo_root.marker @@ -0,0 +1 @@ + diff --git a/codex-rs/utils/cargo-bin/src/lib.rs b/codex-rs/utils/cargo-bin/src/lib.rs index 40fa40c62f..84f69af358 100644 --- a/codex-rs/utils/cargo-bin/src/lib.rs +++ b/codex-rs/utils/cargo-bin/src/lib.rs @@ -1,7 +1,12 @@ use std::ffi::OsString; +use std::io; +use std::path::Path; use std::path::PathBuf; -pub use path_absolutize; +pub use runfiles; + +/// Bazel sets this when runfiles directories are disabled, which we do on all platforms for consistency. +const RUNFILES_MANIFEST_ONLY_ENV: &str = "RUNFILES_MANIFEST_ONLY"; #[derive(Debug, thiserror::Error)] pub enum CargoBinError { @@ -27,10 +32,9 @@ pub enum CargoBinError { /// Returns an absolute path to a binary target built for the current test run. /// -/// In `cargo test`, `CARGO_BIN_EXE_*` env vars are absolute, but Buck2 may set -/// them to project-relative paths (e.g. `buck-out/...`). Those paths break if a -/// test later changes its working directory. This helper makes the path -/// absolute up-front so callers can safely `chdir` afterwards. +/// In `cargo test`, `CARGO_BIN_EXE_*` env vars are absolute. +/// In `bazel test`, `CARGO_BIN_EXE_*` env vars are rlocationpaths, intended to be consumed by `rlocation`. +/// This helper allows callers to transparently support both. pub fn cargo_bin(name: &str) -> Result { let env_keys = cargo_bin_env_keys(name); for key in &env_keys { @@ -38,16 +42,20 @@ pub fn cargo_bin(name: &str) -> Result { return resolve_bin_from_env(key, value); } } - match assert_cmd::Command::cargo_bin(name) { Ok(cmd) => { - let abs = absolutize_from_buck_or_cwd(PathBuf::from(cmd.get_program()))?; - if abs.exists() { - Ok(abs) + let mut path = PathBuf::from(cmd.get_program()); + if !path.is_absolute() { + path = std::env::current_dir() + .map_err(|source| CargoBinError::CurrentDir { source })? + .join(path); + } + if path.exists() { + Ok(path) } else { Err(CargoBinError::ResolvedPathDoesNotExist { key: "assert_cmd::Command::cargo_bin".to_owned(), - path: abs, + path, }) } } @@ -72,6 +80,31 @@ fn cargo_bin_env_keys(name: &str) -> Vec { keys } +pub fn runfiles_available() -> bool { + std::env::var_os(RUNFILES_MANIFEST_ONLY_ENV).is_some() +} + +fn resolve_bin_from_env(key: &str, value: OsString) -> Result { + let raw = PathBuf::from(&value); + if runfiles_available() { + let runfiles = runfiles::Runfiles::create().map_err(|err| CargoBinError::CurrentExe { + source: std::io::Error::other(err), + })?; + if let Some(resolved) = runfiles::rlocation!(runfiles, &raw) + && resolved.exists() + { + return Ok(resolved); + } + } else if raw.is_absolute() && raw.exists() { + return Ok(raw); + } + + Err(CargoBinError::ResolvedPathDoesNotExist { + key: key.to_owned(), + path: raw, + }) +} + /// Macro that derives the path to a test resource at runtime, the value of /// which depends on whether Cargo or Bazel is being used to build and run a /// test. Note the return value may be a relative or absolute path. @@ -84,97 +117,109 @@ fn cargo_bin_env_keys(name: &str) -> Vec { #[macro_export] macro_rules! find_resource { ($resource:expr) => {{ - // When this code is built and run with Bazel: - // - we inject `BAZEL_PACKAGE` as a compile-time environment variable - // that points to native.package_name() - // - at runtime, Bazel will set `RUNFILES_DIR` to the runfiles directory - // - // Therefore, the compile-time value of `BAZEL_PACKAGE` will always be - // included in the compiled binary (even if it is built with Cargo), but - // we only check it at runtime if `RUNFILES_DIR` is set. let resource = std::path::Path::new(&$resource); - match std::env::var("RUNFILES_DIR") { - Ok(bazel_runtime_files) => match option_env!("BAZEL_PACKAGE") { - Some(bazel_package) => { - use $crate::path_absolutize::Absolutize; - - let manifest_dir = std::path::PathBuf::from(bazel_runtime_files) - .join("_main") - .join(bazel_package) - .join(resource); - // Note we also have to normalize (but not canonicalize!) - // the path for _Bazel_ because the original value ends with - // `codex-rs/exec-server/tests/common/../suite/bash`, but - // the `tests/common` folder will not exist at runtime under - // Bazel. As such, we have to normalize it before passing it - // to `dotslash fetch`. - manifest_dir.absolutize().map(|p| p.to_path_buf()) - } - None => Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "BAZEL_PACKAGE not set in Bazel build", - )), - }, - Err(_) => { - let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); - Ok(manifest_dir.join(resource)) - } + if $crate::runfiles_available() { + // When this code is built and run with Bazel: + // - we inject `BAZEL_PACKAGE` as a compile-time environment variable + // that points to native.package_name() + // - at runtime, Bazel will set runfiles-related env vars + $crate::resolve_bazel_runfile(option_env!("BAZEL_PACKAGE"), resource) + } else { + let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + Ok(manifest_dir.join(resource)) } }}; } -fn resolve_bin_from_env(key: &str, value: OsString) -> Result { - let abs = absolutize_from_buck_or_cwd(PathBuf::from(value))?; - - if abs.exists() { - Ok(abs) - } else { - Err(CargoBinError::ResolvedPathDoesNotExist { - key: key.to_owned(), - path: abs, - }) - } -} - -fn absolutize_from_buck_or_cwd(path: PathBuf) -> Result { - if path.is_absolute() { - return Ok(path); - } - - if let Some(root) = - buck_project_root().map_err(|source| CargoBinError::CurrentExe { source })? +pub fn resolve_bazel_runfile( + bazel_package: Option<&str>, + resource: &Path, +) -> std::io::Result { + let runfiles = runfiles::Runfiles::create() + .map_err(|err| std::io::Error::other(format!("failed to create runfiles: {err}")))?; + let runfile_path = match bazel_package { + Some(bazel_package) => PathBuf::from("_main").join(bazel_package).join(resource), + None => { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "BAZEL_PACKAGE was not set at compile time", + )); + } + }; + let runfile_path = normalize_runfile_path(&runfile_path); + if let Some(resolved) = runfiles::rlocation!(runfiles, &runfile_path) + && resolved.exists() { - return Ok(root.join(path)); + return Ok(resolved); } - - Ok(std::env::current_dir() - .map_err(|source| CargoBinError::CurrentDir { source })? - .join(path)) + let runfile_path_display = runfile_path.display(); + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("runfile does not exist at: {runfile_path_display}"), + )) } -/// Best-effort attempt to find the Buck project root for the currently running -/// process. -/// -/// Prefer this over `env!("CARGO_MANIFEST_DIR")` when running under Buck2: our -/// Buck generator sets `CARGO_MANIFEST_DIR="."` for compilation, which makes -/// `env!("CARGO_MANIFEST_DIR")` unusable for locating workspace files. -pub fn buck_project_root() -> Result, std::io::Error> { - if let Some(root) = std::env::var_os("BUCK_PROJECT_ROOT") { - let root = PathBuf::from(root); - if root.is_absolute() { - return Ok(Some(root)); +pub fn resolve_cargo_runfile(resource: &Path) -> std::io::Result { + let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + Ok(manifest_dir.join(resource)) +} + +pub fn repo_root() -> io::Result { + let marker = if runfiles_available() { + let runfiles = runfiles::Runfiles::create() + .map_err(|err| io::Error::other(format!("failed to create runfiles: {err}")))?; + let marker_path = option_env!("CODEX_REPO_ROOT_MARKER") + .map(PathBuf::from) + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + "CODEX_REPO_ROOT_MARKER was not set at compile time", + ) + })?; + runfiles::rlocation!(runfiles, &marker_path).ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + "repo_root.marker not available in runfiles", + ) + })? + } else { + resolve_cargo_runfile(Path::new("repo_root.marker"))? + }; + let mut root = marker; + for _ in 0..4 { + root = root + .parent() + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + "repo_root.marker did not have expected parent depth", + ) + })? + .to_path_buf(); + } + Ok(root) +} + +fn normalize_runfile_path(path: &Path) -> PathBuf { + let mut components = Vec::new(); + for component in path.components() { + match component { + std::path::Component::CurDir => {} + std::path::Component::ParentDir => { + if matches!(components.last(), Some(std::path::Component::Normal(_))) { + components.pop(); + } else { + components.push(component); + } + } + _ => components.push(component), } } - // Fall back to deriving the project root from the location of the test - // runner executable: - // /buck-out/v2/gen/.../__tests__/test-binary - let exe = std::env::current_exe()?; - for ancestor in exe.ancestors() { - if ancestor.file_name().is_some_and(|name| name == "buck-out") { - return Ok(ancestor.parent().map(PathBuf::from)); - } - } - - Ok(None) + components + .into_iter() + .fold(PathBuf::new(), |mut acc, component| { + acc.push(component.as_os_str()); + acc + }) } diff --git a/defs.bzl b/defs.bzl index 249c3a4583..40112969c8 100644 --- a/defs.bzl +++ b/defs.bzl @@ -36,6 +36,8 @@ def codex_rust_crate( crate_edition = None, build_script_data = [], compile_data = [], + lib_data_extra = [], + rustc_env = {}, deps_extra = [], integration_deps_extra = [], integration_compile_data_extra = [], @@ -63,6 +65,8 @@ def codex_rust_crate( You probably don't want this, it's only here for a single caller. build_script_data: Data files exposed to the build script at runtime. compile_data: Non-Rust compile-time data for the library target. + lib_data_extra: Extra runtime data for the library target. + rustc_env: Extra rustc_env entries to merge with defaults. deps_extra: Extra normal deps beyond @crates resolution. Typically only needed when features add additional deps. integration_deps_extra: Extra deps for integration tests only. @@ -85,7 +89,7 @@ def codex_rust_crate( rustc_env = { "BAZEL_PACKAGE": native.package_name(), - } + } | rustc_env binaries = DEP_DATA.get(native.package_name())["binaries"] @@ -112,6 +116,7 @@ def codex_rust_crate( deps = deps, proc_macro_deps = proc_macro_deps, compile_data = compile_data, + data = lib_data_extra, srcs = lib_srcs, edition = crate_edition, rustc_env = rustc_env, @@ -138,7 +143,7 @@ def codex_rust_crate( for binary, main in binaries.items(): #binary = binary.replace("-", "_") sanitized_binaries.append(binary) - cargo_env["CARGO_BIN_EXE_" + binary] = "$(rootpath :%s)" % binary + cargo_env["CARGO_BIN_EXE_" + binary] = "$(rlocationpath :%s)" % binary rust_binary( name = binary, @@ -154,7 +159,7 @@ def codex_rust_crate( for binary_label in extra_binaries: sanitized_binaries.append(binary_label) binary = Label(binary_label).name - cargo_env["CARGO_BIN_EXE_" + binary] = "$(rootpath %s)" % binary_label + cargo_env["CARGO_BIN_EXE_" + binary] = "$(rlocationpath %s)" % binary_label for test in native.glob(["tests/*.rs"], allow_empty = True): test_name = name + "-" + test.removeprefix("tests/").removesuffix(".rs").replace("/", "-") diff --git a/package.json b/package.json index 9f1452a972..633a6f8331 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ }, "engines": { "node": ">=22", - "pnpm": ">=9.0.0" + "pnpm": ">=10.28.0" }, "packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264" } diff --git a/patches/rules_rust.patch b/patches/rules_rust.patch index 298eb74c51..3020ac2282 100644 --- a/patches/rules_rust.patch +++ b/patches/rules_rust.patch @@ -31,4 +31,4 @@ index a28ad50b7..af627fe50 100644 + make_link_flags = _make_link_flags_default_direct if use_direct_link_driver else _make_link_flags_default_indirect return (make_link_flags, get_lib_name) - + diff --git a/shell-tool-mcp/package.json b/shell-tool-mcp/package.json index 24798b2f6e..c6dfcd7a55 100644 --- a/shell-tool-mcp/package.json +++ b/shell-tool-mcp/package.json @@ -3,6 +3,7 @@ "version": "0.0.0-dev", "description": "Codex MCP server for the shell tool with patched Bash and exec wrappers.", "license": "Apache-2.0", + "packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264", "bin": { "codex-shell-tool-mcp": "bin/mcp-server.js" },