From fd7f2aedc72574ae27088b0d312982e0cbb5edf2 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Thu, 12 Feb 2026 00:11:38 -0800 Subject: [PATCH 1/4] Handle response.incomplete (#11558) Treat it same as error. --- codex-rs/codex-api/src/sse/responses.rs | 11 +++++ codex-rs/core/tests/suite/client.rs | 60 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/codex-rs/codex-api/src/sse/responses.rs b/codex-rs/codex-api/src/sse/responses.rs index d7ba3383b7..11e4f9de34 100644 --- a/codex-rs/codex-api/src/sse/responses.rs +++ b/codex-rs/codex-api/src/sse/responses.rs @@ -254,6 +254,17 @@ pub fn process_responses_event( "response.failed event received".into(), ))); } + "response.incomplete" => { + let reason = event.response.as_ref().and_then(|response| { + response + .get("incomplete_details") + .and_then(|details| details.get("reason")) + .and_then(Value::as_str) + }); + let reason = reason.unwrap_or("unknown"); + let message = format!("Incomplete response returned, reason: {reason}"); + return Err(ResponsesEventError::Api(ApiError::Stream(message))); + } "response.completed" => { if let Some(resp_val) = event.response { match serde_json::from_value::(resp_val) { diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index b7b161aa86..ed6912daae 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -37,6 +37,8 @@ use codex_protocol::user_input::UserInput; use core_test_support::load_default_config_for_test; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_completed_with_tokens; +use core_test_support::responses::ev_message_item_added; +use core_test_support::responses::ev_output_text_delta; use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_sse_once; use core_test_support::responses::mount_sse_once_match; @@ -1787,6 +1789,64 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn incomplete_response_emits_content_filter_error_message() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + let server = MockServer::start().await; + + let incomplete_response = sse(vec![ + ev_response_created("resp_incomplete"), + ev_message_item_added("msg_incomplete", "partial content"), + ev_output_text_delta("continued chunk"), + json!({ + "type": "response.incomplete", + "response": { + "id": "resp_incomplete", + "object": "response", + "status": "incomplete", + "error": null, + "incomplete_details": { + "reason": "content_filter" + } + } + }), + ]); + + let responses_mock = mount_sse_once(&server, incomplete_response).await; + + let TestCodex { codex, .. } = test_codex() + .with_config(|config| { + config.model_provider.stream_max_retries = Some(0); + }) + .build(&server) + .await?; + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "trigger incomplete".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + }) + .await?; + + let error_event = wait_for_event(&codex, |ev| matches!(ev, EventMsg::Error(_))).await; + assert!( + matches!( + error_event, + EventMsg::Error(ref err) + if err.message + == "stream disconnected before completion: Incomplete response returned, reason: content_filter" + ), + "expected incomplete content filter error; got {error_event:?}" + ); + + assert_eq!(responses_mock.requests().len(), 1); + + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn azure_overrides_assign_properties_used_for_responses_url() { skip_if_no_network!(); From 6ca9b4327b38bbe5725deb1a464b358c893c4f0f Mon Sep 17 00:00:00 2001 From: xl-openai Date: Thu, 12 Feb 2026 00:17:48 -0800 Subject: [PATCH 2/4] fix: stop inheriting rate-limit limit_name (#11557) When we carry over values from partial rate-limit, we should only do so for the same limit_id. --- codex-rs/core/src/state/session.rs | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/codex-rs/core/src/state/session.rs b/codex-rs/core/src/state/session.rs index 2ceb684d00..a2df7c2398 100644 --- a/codex-rs/core/src/state/session.rs +++ b/codex-rs/core/src/state/session.rs @@ -177,20 +177,15 @@ impl SessionState { } } -// Merge partial rate-limit updates: new fields overwrite existing values; -// missing fields retain prior values. If `limit_id` is absent everywhere, -// default it to `"codex"`. +// Sometimes new snapshots don't include credits or plan information. +// Preserve those from the previous snapshot when missing. For `limit_id`, treat +// missing values as the default `"codex"` bucket. fn merge_rate_limit_fields( previous: Option<&RateLimitSnapshot>, mut snapshot: RateLimitSnapshot, ) -> RateLimitSnapshot { if snapshot.limit_id.is_none() { - snapshot.limit_id = previous - .and_then(|prior| prior.limit_id.clone()) - .or_else(|| Some("codex".to_string())); - } - if snapshot.limit_name.is_none() { - snapshot.limit_name = previous.and_then(|prior| prior.limit_name.clone()); + snapshot.limit_id = Some("codex".to_string()); } if snapshot.credits.is_none() { snapshot.credits = previous.and_then(|prior| prior.credits.clone()); @@ -305,7 +300,7 @@ mod tests { } #[tokio::test] - async fn set_rate_limits_preserves_previous_limit_id_when_missing() { + async fn set_rate_limits_defaults_to_codex_when_limit_id_missing_after_other_bucket() { let session_configuration = make_session_configuration_for_tests().await; let mut state = SessionState::new(session_configuration); @@ -339,12 +334,12 @@ mod tests { .latest_rate_limits .as_ref() .and_then(|v| v.limit_id.clone()), - Some("codex_other".to_string()) + Some("codex".to_string()) ); } #[tokio::test] - async fn set_rate_limits_accepts_new_limit_id_bucket() { + async fn set_rate_limits_carries_credits_and_plan_type_from_codex_to_codex_other() { let session_configuration = make_session_configuration_for_tests().await; let mut state = SessionState::new(session_configuration); @@ -367,7 +362,7 @@ mod tests { state.set_rate_limits(RateLimitSnapshot { limit_id: Some("codex_other".to_string()), - limit_name: Some("codex_other".to_string()), + limit_name: None, primary: Some(RateLimitWindow { used_percent: 30.0, window_minutes: Some(120), @@ -382,7 +377,7 @@ mod tests { state.latest_rate_limits, Some(RateLimitSnapshot { limit_id: Some("codex_other".to_string()), - limit_name: Some("codex_other".to_string()), + limit_name: None, primary: Some(RateLimitWindow { used_percent: 30.0, window_minutes: Some(120), From 26d9bddc52f88d9c88f5dd3740b65f675b7eac42 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 12 Feb 2026 00:56:47 -0800 Subject: [PATCH 3/4] rust-release: exclude cargo-timing.html from release assets (#11564) ## Why The `release` job in `.github/workflows/rust-release.yml` uploads `files: dist/**` via `softprops/action-gh-release`. The downloaded timing artifacts include multiple files with the same basename, `cargo-timing.html` (one per target), which causes release asset collisions/races and can fail with GitHub release-assets API `404 Not Found` errors. ## What Changed - Updated the existing cleanup step before `Create GitHub Release` to remove all `cargo-timing.html` files from `dist/`. - Removed any now-empty directories after deleting those timing files. Relevant change: - https://github.com/openai/codex/blob/daba003d32f299579e9b89240aa8ebdc9f161424/.github/workflows/rust-release.yml#L423 ## Verification - Confirmed from failing release logs that multiple `cargo-timing.html` files were being included in `dist/**` and that the release step failed while operating on duplicate-named assets. - Verified the workflow now deletes those files before the release upload step, so `cargo-timing.html` is no longer part of the release asset set. --- .github/workflows/rust-release.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 7dca3bc70a..3ed2243951 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -424,6 +424,11 @@ jobs: run: | rm -rf dist/shell-tool-mcp* rm -rf dist/windows-binaries* + # cargo-timing.html appears under multiple target-specific directories. + # If included in files: dist/**, release upload races on duplicate + # asset names and can fail with 404s. + find dist -type f -name 'cargo-timing.html' -delete + find dist -type d -empty -delete ls -R dist/ From 4de61af3fbe0e20417e70aab3cd8c5912ce46ac7 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 12 Feb 2026 01:01:18 -0800 Subject: [PATCH 4/4] fix: use x64 windows-2022 runners for code signing --- ...-and-compress-windows-release-artifacts.sh | 72 +++++++++++++ .github/workflows/rust-release-windows.yml | 102 ++---------------- 2 files changed, 81 insertions(+), 93 deletions(-) create mode 100644 .github/scripts/stage-and-compress-windows-release-artifacts.sh diff --git a/.github/scripts/stage-and-compress-windows-release-artifacts.sh b/.github/scripts/stage-and-compress-windows-release-artifacts.sh new file mode 100644 index 0000000000..00cc7743c7 --- /dev/null +++ b/.github/scripts/stage-and-compress-windows-release-artifacts.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ "$#" -lt 1 ]]; then + echo "usage: $0 [ ...]" + exit 1 +fi + +process_target() { + local target="$1" + local release_dir="target/${target}/release" + local dest="dist/${target}" + local repo_root + repo_root="$(pwd)" + + ls -lh "${release_dir}/codex.exe" + ls -lh "${release_dir}/codex-responses-api-proxy.exe" + ls -lh "${release_dir}/codex-windows-sandbox-setup.exe" + ls -lh "${release_dir}/codex-command-runner.exe" + + mkdir -p "$dest" + cp "${release_dir}/codex.exe" "$dest/codex-${target}.exe" + cp "${release_dir}/codex-responses-api-proxy.exe" "$dest/codex-responses-api-proxy-${target}.exe" + cp "${release_dir}/codex-windows-sandbox-setup.exe" "$dest/codex-windows-sandbox-setup-${target}.exe" + cp "${release_dir}/codex-command-runner.exe" "$dest/codex-command-runner-${target}.exe" + + for f in "$dest"/*; do + local base + base="$(basename "$f")" + + if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then + continue + fi + + if [[ "$base" == *.sigstore ]]; then + continue + fi + + tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" + + if [[ "$base" == "codex-${target}.exe" ]]; then + local bundle_dir + local runner_src + local setup_src + bundle_dir="$(mktemp -d)" + runner_src="$dest/codex-command-runner-${target}.exe" + setup_src="$dest/codex-windows-sandbox-setup-${target}.exe" + + if [[ -f "$runner_src" && -f "$setup_src" ]]; then + cp "$dest/$base" "$bundle_dir/$base" + cp "$runner_src" "$bundle_dir/codex-command-runner.exe" + cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe" + (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) + else + echo "warning: missing sandbox binaries; falling back to single-binary zip" + echo "warning: expected $runner_src and $setup_src" + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + + rm -rf "$bundle_dir" + else + (cd "$dest" && 7z a "${base}.zip" "$base") + fi + + "${GITHUB_WORKSPACE}/.github/workflows/zstd" -T0 -19 "$dest/$base" + done +} + +for target in "$@"; do + process_target "$target" +done diff --git a/.github/workflows/rust-release-windows.yml b/.github/workflows/rust-release-windows.yml index fd411715be..a3f736fcd8 100644 --- a/.github/workflows/rust-release-windows.yml +++ b/.github/workflows/rust-release-windows.yml @@ -121,8 +121,8 @@ jobs: build-windows: needs: - build-windows-binaries - name: Build - ${{ matrix.runner }} - ${{ matrix.target }} - runs-on: ${{ matrix.runs_on }} + name: Build - windows-x64 - ${{ matrix.target }} + runs-on: windows-2022 timeout-minutes: 60 permissions: contents: read @@ -135,16 +135,8 @@ jobs: fail-fast: false matrix: include: - - runner: windows-x64 - target: x86_64-pc-windows-msvc - runs_on: - group: codex-runners - labels: codex-windows-x64 - - runner: windows-arm64 - target: aarch64-pc-windows-msvc - runs_on: - group: codex-runners - labels: codex-windows-arm64 + - target: x86_64-pc-windows-msvc + - target: aarch64-pc-windows-msvc steps: - uses: actions/checkout@v6 @@ -161,14 +153,8 @@ jobs: name: windows-binaries-${{ matrix.target }}-helpers path: codex-rs/target/${{ matrix.target }}/release - - name: Verify binaries - shell: bash - run: | - set -euo pipefail - ls -lh target/${{ matrix.target }}/release/codex.exe - ls -lh target/${{ matrix.target }}/release/codex-responses-api-proxy.exe - ls -lh target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe - ls -lh target/${{ matrix.target }}/release/codex-command-runner.exe + - name: Install DotSlash + uses: facebook/install-dotslash@v2 - name: Sign Windows binaries with Azure Trusted Signing uses: ./.github/actions/windows-code-sign @@ -181,81 +167,11 @@ jobs: account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} - - name: Stage artifacts + - name: Stage and compress Windows release artifacts shell: bash run: | - dest="dist/${{ matrix.target }}" - mkdir -p "$dest" - - cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe" - cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe" - cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" - cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe" - - - name: Install DotSlash - uses: facebook/install-dotslash@v2 - - - name: Compress artifacts - shell: bash - run: | - # Path that contains the uncompressed binaries for the current - # ${{ matrix.target }} - dest="dist/${{ matrix.target }}" - repo_root=$PWD - - # For compatibility with environments that lack the `zstd` tool we - # additionally create a `.tar.gz` and `.zip` for every Windows binary. - # The end result is: - # codex-.zst - # codex-.tar.gz - # codex-.zip - for f in "$dest"/*; do - base="$(basename "$f")" - # Skip files that are already archives (shouldn't happen, but be - # safe). - if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then - continue - fi - - # Don't try to compress signature bundles. - if [[ "$base" == *.sigstore ]]; then - continue - fi - - # Create per-binary tar.gz - tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" - - # Create zip archive for Windows binaries. - # Must run from inside the dest dir so 7z won't embed the - # directory path inside the zip. - if [[ "$base" == "codex-${{ matrix.target }}.exe" ]]; then - # Bundle the sandbox helper binaries into the main codex zip so - # WinGet installs include the required helpers next to codex.exe. - # Fall back to the single-binary zip if the helpers are missing - # to avoid breaking releases. - bundle_dir="$(mktemp -d)" - runner_src="$dest/codex-command-runner-${{ matrix.target }}.exe" - setup_src="$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" - if [[ -f "$runner_src" && -f "$setup_src" ]]; then - cp "$dest/$base" "$bundle_dir/$base" - cp "$runner_src" "$bundle_dir/codex-command-runner.exe" - cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe" - # Use an absolute path so bundle zips land in the real dist - # dir even when 7z runs from a temp directory. - (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) - else - echo "warning: missing sandbox binaries; falling back to single-binary zip" - echo "warning: expected $runner_src and $setup_src" - (cd "$dest" && 7z a "${base}.zip" "$base") - fi - rm -rf "$bundle_dir" - else - (cd "$dest" && 7z a "${base}.zip" "$base") - fi - - # Keep raw executables and produce .zst alongside them. - "${GITHUB_WORKSPACE}/.github/workflows/zstd" -T0 -19 "$dest/$base" - done + bash "$GITHUB_WORKSPACE/.github/scripts/stage-and-compress-windows-release-artifacts.sh" \ + ${{ matrix.target }} - uses: actions/upload-artifact@v6 with: