From 6f203b9746e72e8d315d3de462adbcd25bf1c88c Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Thu, 11 Jun 2026 04:13:36 +0800 Subject: [PATCH] {"schema":"decodex/commit/1","summary":"Persist upstream Radar review artifacts","authority":"manual"} --- .../github/bundles/openai-codex-pr-25147.json | 259 +++ .../github/bundles/openai-codex-pr-26701.json | 121 ++ .../github/bundles/openai-codex-pr-26734.json | 232 +++ .../github/bundles/openai-codex-pr-27129.json | 130 ++ .../github/impact/openai-codex-pr-25147.json | 46 + .../github/impact/openai-codex-pr-26701.json | 46 + .../github/impact/openai-codex-pr-26734.json | 46 + .../github/impact/openai-codex-pr-27129.json | 46 + .../review-queue/openai-codex-latest.json | 1409 +++++++++-------- .../reviews/openai-codex-pr-25147.review.json | 97 ++ .../reviews/openai-codex-pr-26701.review.json | 76 + .../reviews/openai-codex-pr-26734.review.json | 76 + .../reviews/openai-codex-pr-27129.review.json | 67 + .../openai-codex-pr-25147.json | 54 + .../openai-codex-pr-26701.json | 54 + .../openai-codex-pr-26734.json | 54 + .../openai-codex-pr-27129.json | 54 + 17 files changed, 2178 insertions(+), 689 deletions(-) create mode 100644 artifacts/github/bundles/openai-codex-pr-25147.json create mode 100644 artifacts/github/bundles/openai-codex-pr-26701.json create mode 100644 artifacts/github/bundles/openai-codex-pr-26734.json create mode 100644 artifacts/github/bundles/openai-codex-pr-27129.json create mode 100644 artifacts/github/impact/openai-codex-pr-25147.json create mode 100644 artifacts/github/impact/openai-codex-pr-26701.json create mode 100644 artifacts/github/impact/openai-codex-pr-26734.json create mode 100644 artifacts/github/impact/openai-codex-pr-27129.json create mode 100644 artifacts/github/reviews/openai-codex-pr-25147.review.json create mode 100644 artifacts/github/reviews/openai-codex-pr-26701.review.json create mode 100644 artifacts/github/reviews/openai-codex-pr-26734.review.json create mode 100644 artifacts/github/reviews/openai-codex-pr-27129.review.json create mode 100644 artifacts/github/social-candidates/openai-codex-pr-25147.json create mode 100644 artifacts/github/social-candidates/openai-codex-pr-26701.json create mode 100644 artifacts/github/social-candidates/openai-codex-pr-26734.json create mode 100644 artifacts/github/social-candidates/openai-codex-pr-27129.json diff --git a/artifacts/github/bundles/openai-codex-pr-25147.json b/artifacts/github/bundles/openai-codex-pr-25147.json new file mode 100644 index 000000000..822868b1f --- /dev/null +++ b/artifacts/github/bundles/openai-codex-pr-25147.json @@ -0,0 +1,259 @@ +{ + "analysis_mode": "pr_first", + "commits": [ + { + "author": "ssetty-oai", + "committed_at": "2026-05-29T20:16:10Z", + "message": "Retry streamable HTTP initialize failures", + "sha": "09ce7e176bb3e35108cce7c74f9fe566125b6c87", + "url": "https://github.com/openai/codex/commit/09ce7e176bb3e35108cce7c74f9fe566125b6c87" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-04T00:19:08Z", + "message": "Add MCP initialize outcome metric", + "sha": "821b679505e5021c4fc20a3c79070d96ff0aaa24", + "url": "https://github.com/openai/codex/commit/821b679505e5021c4fc20a3c79070d96ff0aaa24" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-04T23:54:41Z", + "message": "Retry remote streamable HTTP no-response failures", + "sha": "e992bdf9805440b068ffa2c031dbf3be0e244107", + "url": "https://github.com/openai/codex/commit/e992bdf9805440b068ffa2c031dbf3be0e244107" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-05T00:03:49Z", + "message": "codex: fix CI fmt-check recipe on PR #25147", + "sha": "7c54723762ae9ebb3d2bbb95e71ac5000771c9c9", + "url": "https://github.com/openai/codex/commit/7c54723762ae9ebb3d2bbb95e71ac5000771c9c9" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-05T00:36:07Z", + "message": "codex: fix CI failures on PR #25147", + "sha": "e4dca2440722489eb1d774ba0b273a4260f8d0e8", + "url": "https://github.com/openai/codex/commit/e4dca2440722489eb1d774ba0b273a4260f8d0e8" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-05T00:54:13Z", + "message": "codex: address PR review feedback (#25147)", + "sha": "5f2b14e092e169c3f4c9f4b93c40e2560f95e2ca", + "url": "https://github.com/openai/codex/commit/5f2b14e092e169c3f4c9f4b93c40e2560f95e2ca" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-05T01:08:17Z", + "message": "codex: address follow-up review feedback (#25147)", + "sha": "d4b826175776c26f977b2e10a9dc9bc4a3030e41", + "url": "https://github.com/openai/codex/commit/d4b826175776c26f977b2e10a9dc9bc4a3030e41" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-05T01:15:31Z", + "message": "codex: fix python SDK signature test (#25147)", + "sha": "daf24cccd232bb9cc3035eb9985d2bf3ee24cab5", + "url": "https://github.com/openai/codex/commit/daf24cccd232bb9cc3035eb9985d2bf3ee24cab5" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-05T01:30:15Z", + "message": "codex: narrow retry PR scope (#25147)", + "sha": "b0a300fb9b9d8ee73be8d40f718eb7b9464a28be", + "url": "https://github.com/openai/codex/commit/b0a300fb9b9d8ee73be8d40f718eb7b9464a28be" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-05T01:31:21Z", + "message": "codex: remove SDK changes from retry PR (#25147)", + "sha": "6e4c8dfa31a5fbb6216e8465337d737b7327fb6a", + "url": "https://github.com/openai/codex/commit/6e4c8dfa31a5fbb6216e8465337d737b7327fb6a" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-05T01:58:16Z", + "message": "codex: address retry review structure feedback (#25147)", + "sha": "dee789ab91d6883ca16fbc86aeadbf524d95c1ef", + "url": "https://github.com/openai/codex/commit/dee789ab91d6883ca16fbc86aeadbf524d95c1ef" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-05T02:03:37Z", + "message": "codex: fix justfile formatting for CI (#25147)", + "sha": "08503d6d13e7b7efc817299c30bcf34bd7b5b076", + "url": "https://github.com/openai/codex/commit/08503d6d13e7b7efc817299c30bcf34bd7b5b076" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-05T02:16:56Z", + "message": "codex: address streamable http retry review feedback (#25147)", + "sha": "75e3421f94a530314154b3ff9dd51a1064f861f6", + "url": "https://github.com/openai/codex/commit/75e3421f94a530314154b3ff9dd51a1064f861f6" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-05T02:22:37Z", + "message": "codex: keep rmcp retry module surgical (#25147)", + "sha": "d1596f4595d17a2e2a94a8fc45c7ceb116079868", + "url": "https://github.com/openai/codex/commit/d1596f4595d17a2e2a94a8fc45c7ceb116079868" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-05T02:37:57Z", + "message": "codex: narrow streamable http initialize retry (#25147)", + "sha": "2e23b88414a393e9800c328bded2e6f2bd7ba7a7", + "url": "https://github.com/openai/codex/commit/2e23b88414a393e9800c328bded2e6f2bd7ba7a7" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-09T15:01:20Z", + "message": "Persist OAuth state before initialize retry", + "sha": "b8760d31e0e00e07aa8db22afae4ab3fb9797af4", + "url": "https://github.com/openai/codex/commit/b8760d31e0e00e07aa8db22afae4ab3fb9797af4" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-09T15:06:16Z", + "message": "Remove python SDK changes from retry PR", + "sha": "1f6a75d2ac149df0a0d8f99a531aeb65243c5d1b", + "url": "https://github.com/openai/codex/commit/1f6a75d2ac149df0a0d8f99a531aeb65243c5d1b" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-09T15:26:09Z", + "message": "Retry streamable HTTP recovery handshakes", + "sha": "9dfb4be0ec69162ea9b1b10008c160a333a99df1", + "url": "https://github.com/openai/codex/commit/9dfb4be0ec69162ea9b1b10008c160a333a99df1" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-09T15:44:15Z", + "message": "Retry tools list transient HTTP failures", + "sha": "ce34025915ff7df1c060ff0343c5b42c4f3453f9", + "url": "https://github.com/openai/codex/commit/ce34025915ff7df1c060ff0343c5b42c4f3453f9" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-09T16:00:11Z", + "message": "Retry initialized notification failures", + "sha": "7a381f591d4bd26aabb5312d89b5c5b3d47d5a9d", + "url": "https://github.com/openai/codex/commit/7a381f591d4bd26aabb5312d89b5c5b3d47d5a9d" + }, + { + "author": "ssetty-oai", + "committed_at": "2026-06-09T16:14:51Z", + "message": "Cover tools list JSON-RPC retry", + "sha": "4f29a9e89e8f39e784336e83bc71dffc270f632d", + "url": "https://github.com/openai/codex/commit/4f29a9e89e8f39e784336e83bc71dffc270f632d" + } + ], + "default_branch": "main", + "docs_refs": [], + "examples_refs": [], + "extracted_flags": [ + "HTTP", + "RMCP", + "--package", + "--test", + "MCP", + "SDK", + "JSON", + "MEMO_URI", + "MEMO_CONTENT", + "MCP_SESSION_ID_HEADER", + "SESSION_POST_FAILURE_CONTROL_PATH", + "INITIALIZE_POST_FAILURE_CONTROL_PATH", + "INITIALIZED_NOTIFICATION_POST_FAILURE_CONTROL_PATH", + "MAX_MCP_POST_BODY_BYTES", + "WWW", + "MAX_BIND_RETRIES", + "BIND_RETRY_DELAY", + "MCP_EXPECT_BEARER", + "BAD_REQUEST", + "NO_CONTENT", + "POST", + "CONTENT_TYPE", + "HEADER_SESSION_ID", + "JSON_MIME_TYPE", + "EVENT_STREAM_MIME_TYPE", + "REQUEST_TIMEOUT", + "TOO_MANY_REQUESTS", + "INTERNAL_SERVER_ERROR", + "BAD_GATEWAY", + "SERVICE_UNAVAILABLE", + "GATEWAY_TIMEOUT", + "STREAMABLE_HTTP_RETRY_DELAYS_MS", + "JSON_RPC_INTERNAL_ERROR_CODE", + "SIMULATED_NO_RESPONSE_MESSAGE" + ], + "files": [ + { + "additions": 103, + "deletions": 16, + "patch_excerpt": "@@ -8,6 +8,7 @@ use std::time::Duration;\n \n use axum::Router;\n use axum::body::Body;\n+use axum::body::to_bytes;\n use axum::extract::Json;\n use axum::extract::State;\n use axum::http::HeaderMap;\n@@ -48,6 +49,7 @@ use rmcp::transport::StreamableHttpServerConfig;\n use rmcp::transport::StreamableHttpService;\n use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;\n use serde::Deserialize;\n+use serde_json::Value;\n use serde_json::json;\n use tokio::sync::Mutex;\n use tokio::task;\n@@ -64,18 +66,32 @@ const MEMO_URI: &str = \"memo://codex/example-note\";\n const MEMO_CONTENT: &str = \"This is a sample MCP resource served by the rmcp test server.\";\n const MCP_SESSION_ID_HEADER: &str = \"mcp-session-id\";\n const SESSION_POST_FAILURE_CONTROL_PATH: &str = \"/test/control/session-post-failure\";\n+const INITIALIZE_POST_FAILURE_CONTROL_PATH: &str = \"/test/control/initialize-post-failure...", + "path": "codex-rs/rmcp-client/src/bin/test_streamable_http_server.rs", + "status": "modified" + }, + { + "additions": 71, + "deletions": 1, + "patch_excerpt": "@@ -28,6 +28,8 @@ use reqwest::header::CONTENT_TYPE;\n use reqwest::header::HeaderMap;\n use reqwest::header::HeaderName;\n use rmcp::model::ClientJsonRpcMessage;\n+use rmcp::model::ClientNotification;\n+use rmcp::model::ConstString;\n use rmcp::model::JsonRpcMessage;\n use rmcp::model::ServerJsonRpcMessage;\n use rmcp::transport::streamable_http_client::AuthRequiredError;\n@@ -185,6 +187,25 @@ impl StreamableHttpClient for StreamableHttpClientAdapter {\n \n let content_type = response_header(&response.headers, CONTENT_TYPE);\n let session_id = response_header(&response.headers, HEADER_SESSION_ID);\n+ if !status_is_success(response.status) {\n+ let body = collect_body(&mut body_stream).await?;\n+ if !retryable_post_response_status(mcp_method.as_deref(), response.status)\n+ && content_type\n+ .as_deref()\n+ .i...", + "path": "codex-rs/rmcp-client/src/http_client_adapter.rs", + "status": "modified" + }, + { + "additions": 137, + "deletions": 17, + "patch_excerpt": "@@ -74,6 +74,13 @@ use crate::utils::apply_default_headers;\n use crate::utils::build_default_headers;\n use codex_config::types::OAuthCredentialsStoreMode;\n \n+#[path = \"streamable_http_retry.rs\"]\n+mod streamable_http_retry;\n+\n+use self::streamable_http_retry::HandshakeError;\n+use self::streamable_http_retry::STREAMABLE_HTTP_RETRY_DELAYS_MS;\n+use self::streamable_http_retry::sleep_with_retry_deadline;\n+\n enum PendingTransport {\n InProcess {\n transport: tokio::io::DuplexStream,\n@@ -223,6 +230,25 @@ enum ClientOperationError {\n Timeout { label: String, duration: Duration },\n }\n \n+fn remaining_operation_timeout(\n+ label: &str,\n+ timeout: Option,\n+ deadline: Option,\n+) -> std::result::Result, ClientOperationError> {\n+ let Some(deadline) = deadline else {\n+ return Ok(None);\n+ };\n+ let remaining = deadline.saturating_du...", + "path": "codex-rs/rmcp-client/src/rmcp_client.rs", + "status": "modified" + }, + { + "additions": 239, + "deletions": 0, + "patch_excerpt": "@@ -0,0 +1,239 @@\n+use std::sync::Arc;\n+use std::time::Duration;\n+use std::time::Instant;\n+\n+use anyhow::Result;\n+use anyhow::anyhow;\n+use codex_exec_server::ExecServerError;\n+use reqwest::StatusCode;\n+use rmcp::service::RoleClient;\n+use rmcp::service::RunningService;\n+use rmcp::transport::streamable_http_client::StreamableHttpError;\n+use tokio::time;\n+use tracing::warn;\n+\n+use crate::elicitation_client_service::ElicitationClientService;\n+use crate::http_client_adapter::StreamableHttpClientAdapterError;\n+use crate::oauth::OAuthPersistor;\n+\n+use super::PendingTransport;\n+use super::RmcpClient;\n+\n+const JSON_RPC_INTERNAL_ERROR_CODE: i64 = -32603;\n+pub(super) const STREAMABLE_HTTP_RETRY_DELAYS_MS: [u64; 2] = [250, 1_000];\n+\n+impl RmcpClient {\n+ pub(super) async fn connect_pending_transport_with_initialize_retries(\n+ &self,\n+ initial_transport: PendingTransport,\n+ cli...", + "path": "codex-rs/rmcp-client/src/streamable_http_retry.rs", + "status": "added" + }, + { + "additions": 74, + "deletions": 0, + "patch_excerpt": "@@ -0,0 +1,74 @@\n+use std::any::TypeId;\n+\n+use codex_exec_server::ExecServerError;\n+use pretty_assertions::assert_eq;\n+use rmcp::transport::DynamicTransportError;\n+use rmcp::transport::streamable_http_client::StreamableHttpError;\n+\n+use crate::http_client_adapter::StreamableHttpClientAdapterError;\n+\n+use super::*;\n+\n+#[test]\n+fn retryable_initialize_error_includes_initialized_notification_context() {\n+ let contexts = [\n+ \"send initialize request\",\n+ \"send initialized notification\",\n+ \"receive initialize response\",\n+ ];\n+\n+ assert_eq!(\n+ contexts.map(|context| {\n+ RmcpClient::is_retryable_client_initialize_error(&retryable_initialize_error(context))\n+ }),\n+ [true, true, false],\n+ );\n+}\n+\n+#[test]\n+fn retryable_streamable_http_error_includes_remote_body_stream_failure() {\n+ let errors = [\n+ StreamableHttpError::Clie...", + "path": "codex-rs/rmcp-client/src/streamable_http_retry_tests.rs", + "status": "added" + }, + { + "additions": 247, + "deletions": 4, + "patch_excerpt": "@@ -1,13 +1,229 @@\n mod streamable_http_test_support;\n \n+use std::sync::Arc;\n+use std::sync::atomic::AtomicUsize;\n+use std::sync::atomic::Ordering;\n+use std::time::Duration;\n+\n+use codex_exec_server::Environment;\n+use codex_exec_server::ExecServerError;\n+use codex_exec_server::HttpClient;\n+use codex_exec_server::HttpRequestParams;\n+use codex_exec_server::HttpRequestResponse;\n+use codex_exec_server::HttpResponseBodyStream;\n+use futures::FutureExt as _;\n+use futures::future::BoxFuture;\n use pretty_assertions::assert_eq;\n+use serde_json::Value;\n \n+use streamable_http_test_support::arm_initialize_post_failure;\n+use streamable_http_test_support::arm_initialize_post_json_rpc_failure;\n+use streamable_http_test_support::arm_initialized_notification_post_json_rpc_failure;\n use streamable_http_test_support::arm_session_post_failure;\n+use streamable_http_test_support::arm_session_post_json_rpc_fail...", + "path": "codex-rs/rmcp-client/tests/streamable_http_recovery.rs", + "status": "modified" + }, + { + "additions": 114, + "deletions": 1, + "patch_excerpt": "@@ -20,6 +20,7 @@ use anyhow::Context as _;\n use codex_config::types::OAuthCredentialsStoreMode;\n use codex_exec_server::Environment;\n use codex_exec_server::ExecServerClient;\n+use codex_exec_server::HttpClient;\n use codex_exec_server::RemoteExecServerConnectArgs;\n use codex_rmcp_client::ElicitationAction;\n use codex_rmcp_client::ElicitationResponse;\n@@ -44,6 +45,9 @@ use tokio::process::Command;\n use tokio::time::sleep;\n \n const SESSION_POST_FAILURE_CONTROL_PATH: &str = \"/test/control/session-post-failure\";\n+const INITIALIZE_POST_FAILURE_CONTROL_PATH: &str = \"/test/control/initialize-post-failure\";\n+const INITIALIZED_NOTIFICATION_POST_FAILURE_CONTROL_PATH: &str =\n+ \"/test/control/initialized-notification-post-failure\";\n \n fn streamable_http_server_bin() -> Result {\n codex_utils_cargo_bin::cargo_bin(\"test_streamable_http_server\")\n@@ -74,14 +78,22 @@ pub(cra...", + "path": "codex-rs/rmcp-client/tests/streamable_http_test_support.rs", + "status": "modified" + } + ], + "linked_issues": [ + "#25147" + ], + "notes": [ + "Built from GitHub pull-request, commits, files, and repo endpoints." + ], + "primary_pr": { + "body": "## Summary\n- Retry transient streamable HTTP failures during RMCP startup when the failure happens while sending the initialize request.\n- Retry transient streamable HTTP failures for tools/list, which is read-only and safe to replay.\n- Cover both retryable HTTP statuses and request-layer failures where no HTTP status is returned.\n- Surface retryable HTTP statuses from the streamable HTTP adapter as typed client errors.\n- Add integration coverage for initialize retry, tools/list retry, no-status request failure retry, and non-retryable initialize status.\n\n## Root cause\nThe observed codex_apps failures can happen before normal tool execution: RMCP startup fails while sending initialize, or the first read-only tools/list fails after startup. Retrying hosted_apps_bridge tools/call would not cover initialize and would risk replaying side-effecting tool calls. This change retries the streamable HTTP handshake itself, recreates the transport between initialize attempts, and retries only tools/list among post-initialize service operations.\n\n## Validation\n- cargo fmt --package codex-rmcp-client\n- cargo test -p codex-rmcp-client --test streamable_http_recovery", + "labels": [], + "merged_at": "2026-06-09T22:49:49Z", + "number": 25147, + "state": "merged", + "title": "[codex] Retry streamable HTTP initialize failures", + "url": "https://github.com/openai/codex/pull/25147" + }, + "repo": "openai/codex", + "schema": "github_change_bundle/v1" +} diff --git a/artifacts/github/bundles/openai-codex-pr-26701.json b/artifacts/github/bundles/openai-codex-pr-26701.json new file mode 100644 index 000000000..e53df7e9e --- /dev/null +++ b/artifacts/github/bundles/openai-codex-pr-26701.json @@ -0,0 +1,121 @@ +{ + "analysis_mode": "pr_first", + "commits": [ + { + "author": "canvrno-oai", + "committed_at": "2026-06-03T18:10:15Z", + "message": "init", + "sha": "52f9ec871bedbd53a0c0741d0b7bc47e3dae3da9", + "url": "https://github.com/openai/codex/commit/52f9ec871bedbd53a0c0741d0b7bc47e3dae3da9" + }, + { + "author": "canvrno-oai", + "committed_at": "2026-06-03T22:09:21Z", + "message": "cleanup", + "sha": "4d32a96c7580b022e5d906bcbb2eb283b80338f7", + "url": "https://github.com/openai/codex/commit/4d32a96c7580b022e5d906bcbb2eb283b80338f7" + }, + { + "author": "canvrno-oai", + "committed_at": "2026-06-03T23:18:35Z", + "message": "Adjustment for admin-disabled plugins in /plugin menu", + "sha": "ddd48cab1b5d617e8a01737c0665f4025b351421", + "url": "https://github.com/openai/codex/commit/ddd48cab1b5d617e8a01737c0665f4025b351421" + }, + { + "author": "canvrno-oai", + "committed_at": "2026-06-04T00:23:54Z", + "message": "fix", + "sha": "44448048dcad4a75a1d7056f23cc87691438268c", + "url": "https://github.com/openai/codex/commit/44448048dcad4a75a1d7056f23cc87691438268c" + }, + { + "author": "canvrno-oai", + "committed_at": "2026-06-06T00:41:49Z", + "message": "tui: update remote plugin detail fixtures", + "sha": "ccf0f0d9892fd9b253401466d6dee7a1886ac407", + "url": "https://github.com/openai/codex/commit/ccf0f0d9892fd9b253401466d6dee7a1886ac407" + }, + { + "author": "canvrno-oai", + "committed_at": "2026-06-09T20:54:23Z", + "message": "Merge branch 'main' into canvrno/plugin_sharing_tui_stack_pr1_remote_identity", + "sha": "9da50461f3d23990d484b3ba29b5cb293269d23f", + "url": "https://github.com/openai/codex/commit/9da50461f3d23990d484b3ba29b5cb293269d23f" + } + ], + "default_branch": "main", + "docs_refs": [], + "examples_refs": [], + "extracted_flags": [ + "TUI", + "OPENAI_CURATED_MARKETPLACE_NAME", + "NONE" + ], + "files": [ + { + "additions": 1, + "deletions": 0, + "patch_excerpt": "@@ -11,6 +11,7 @@ use crate::app_event::ExitMode;\n use crate::app_event::FeedbackCategory;\n use crate::app_event::HistoryLookupResponse;\n use crate::app_event::PermissionProfileSelection;\n+use crate::app_event::PluginLocation;\n use crate::app_event::RateLimitRefreshOrigin;\n use crate::app_event::RealtimeAudioDeviceKind;\n #[cfg(target_os = \"windows\")]", + "path": "codex-rs/tui/src/app.rs", + "status": "modified" + }, + { + "additions": 28, + "deletions": 7, + "patch_excerpt": "@@ -241,22 +241,22 @@ impl App {\n &mut self,\n app_server: &AppServerSession,\n cwd: PathBuf,\n- marketplace_path: AbsolutePathBuf,\n+ location: PluginLocation,\n plugin_name: String,\n plugin_display_name: String,\n ) {\n let request_handle = app_server.request_handle();\n let app_event_tx = self.app_event_tx.clone();\n tokio::spawn(async move {\n let cwd_for_event = cwd.clone();\n- let marketplace_path_for_event = marketplace_path.clone();\n+ let location_for_event = location.clone();\n let plugin_name_for_event = plugin_name.clone();\n- let result = fetch_plugin_install(request_handle, marketplace_path, plugin_name)\n+ let result = fetch_plugin_install(request_handle, location, plugin_name)\n .await\n .map_err(|err| format!...", + "path": "codex-rs/tui/src/app/background_requests.rs", + "status": "modified" + }, + { + "additions": 8, + "deletions": 6, + "patch_excerpt": "@@ -573,14 +573,14 @@ impl App {\n }\n AppEvent::FetchPluginInstall {\n cwd,\n- marketplace_path,\n+ location,\n plugin_name,\n plugin_display_name,\n } => {\n self.fetch_plugin_install(\n app_server,\n cwd,\n- marketplace_path,\n+ location,\n plugin_name,\n plugin_display_name,\n );\n@@ -601,7 +601,7 @@ impl App {\n }\n AppEvent::PluginInstallLoaded {\n cwd,\n- marketplace_path,\n+ location,\n plugin_name,\n plugin_display_name,\n result,\n@@ -612,7 +612,7 @@ impl App {\n }\n let should_refresh_plugin_detail...", + "path": "codex-rs/tui/src/app/event_dispatch.rs", + "status": "modified" + }, + { + "additions": 17, + "deletions": 2, + "patch_excerpt": "@@ -109,6 +109,21 @@ pub(crate) struct ConnectorsSnapshot {\n pub(crate) connectors: Vec,\n }\n \n+#[derive(Debug, Clone, PartialEq, Eq)]\n+pub(crate) enum PluginLocation {\n+ Local { marketplace_path: AbsolutePathBuf },\n+ Remote { marketplace_name: String },\n+}\n+\n+impl PluginLocation {\n+ pub(crate) fn into_request_params(self) -> (Option, Option) {\n+ match self {\n+ PluginLocation::Local { marketplace_path } => (Some(marketplace_path), None),\n+ PluginLocation::Remote { marketplace_name } => (None, Some(marketplace_name)),\n+ }\n+ }\n+}\n+\n /// Distinguishes why a rate-limit refresh was requested so the completion\n /// handler can route the result correctly.\n ///\n@@ -493,15 +508,15 @@ pub(crate) enum AppEvent {\n /// Install a specific plugin from a marketplace.\n FetchPluginInstall {\n cwd: PathBuf,\n...", + "path": "codex-rs/tui/src/app_event.rs", + "status": "modified" + }, + { + "additions": 146, + "deletions": 59, + "patch_excerpt": "@@ -5,6 +5,7 @@ use std::time::Instant;\n \n use super::ChatWidget;\n use crate::app_event::AppEvent;\n+use crate::app_event::PluginLocation;\n use crate::bottom_pane::ColumnWidthMode;\n use crate::bottom_pane::SelectionAction;\n use crate::bottom_pane::SelectionItem;\n@@ -25,17 +26,18 @@ use crate::tui::FrameRequester;\n use codex_app_server_protocol::MarketplaceAddResponse;\n use codex_app_server_protocol::MarketplaceRemoveResponse;\n use codex_app_server_protocol::MarketplaceUpgradeResponse;\n+use codex_app_server_protocol::PluginAvailability;\n use codex_app_server_protocol::PluginDetail;\n use codex_app_server_protocol::PluginInstallPolicy;\n use codex_app_server_protocol::PluginInstallResponse;\n use codex_app_server_protocol::PluginListResponse;\n use codex_app_server_protocol::PluginMarketplaceEntry;\n use codex_app_server_protocol::PluginReadResponse;\n+use codex_app_server_protocol::PluginSource;...", + "path": "codex-rs/tui/src/chatwidget/plugins.rs", + "status": "modified" + }, + { + "additions": 28, + "deletions": 0, + "patch_excerpt": "@@ -1337,6 +1337,34 @@ pub(super) fn plugins_test_summary(\n }\n }\n \n+pub(super) fn plugins_test_remote_summary(\n+ remote_plugin_id: &str,\n+ name: &str,\n+ display_name: Option<&str>,\n+ description: Option<&str>,\n+ installed: bool,\n+) -> PluginSummary {\n+ PluginSummary {\n+ id: remote_plugin_id.to_string(),\n+ remote_plugin_id: Some(remote_plugin_id.to_string()),\n+ local_version: None,\n+ name: name.to_string(),\n+ share_context: None,\n+ source: PluginSource::Remote,\n+ installed,\n+ enabled: true,\n+ install_policy: PluginInstallPolicy::Available,\n+ auth_policy: PluginAuthPolicy::OnInstall,\n+ availability: PluginAvailability::Available,\n+ interface: Some(plugins_test_interface(\n+ display_name,\n+ description,\n+ /*long_description*/ None,\n+ )),\n+ k...", + "path": "codex-rs/tui/src/chatwidget/tests/helpers.rs", + "status": "modified" + }, + { + "additions": 345, + "deletions": 0, + "patch_excerpt": "@@ -6,6 +6,7 @@ use codex_app_server_protocol::HookErrorInfo;\n use codex_app_server_protocol::HooksListEntry;\n use codex_app_server_protocol::HooksListResponse;\n use codex_app_server_protocol::MarketplaceRemoveResponse;\n+use codex_app_server_protocol::PluginAvailability;\n use codex_features::Stage;\n use pretty_assertions::assert_eq;\n \n@@ -714,6 +715,350 @@ async fn plugin_detail_popup_hides_disclosure_for_installed_plugins() {\n );\n }\n \n+#[tokio::test]\n+async fn plugins_popup_remote_row_opens_remote_detail() {\n+ let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;\n+ chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);\n+\n+ let popup = render_loaded_plugins_popup(\n+ &mut chat,\n+ plugins_test_response(vec![PluginMarketplaceEntry {\n+ name: \"workspace-directory\".to_string(),\n+ path: None,\n+ ...", + "path": "codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs", + "status": "modified" + } + ], + "linked_issues": [], + "notes": [ + "Built from GitHub pull-request, commits, files, and repo endpoints." + ], + "primary_pr": { + "body": "This starts the shared-plugin TUI stack by teaching the existing plugin popup flows to address plugins from either a local marketplace path or a remote marketplace name. The visible surface stays intentionally narrow so later catalog work can build on stable remote request identity without adding share-management UI.\r\n\r\n- Adds remote-capable detail/install/uninstall request routing for plugin popup actions.\r\n- Uses remote plugin IDs for remote install and uninstall while preserving local plugin IDs and paths.\r\n- Keeps remote detail views available when a marketplace has no filesystem path.\r\n- Blocks admin-disabled plugin installs and toggle affordances in the existing flows.", + "labels": [], + "merged_at": "2026-06-09T23:34:39Z", + "number": 26701, + "state": "merged", + "title": "TUI Plugin Sharing 1 - add remote plugin identity", + "url": "https://github.com/openai/codex/pull/26701" + }, + "repo": "openai/codex", + "schema": "github_change_bundle/v1" +} diff --git a/artifacts/github/bundles/openai-codex-pr-26734.json b/artifacts/github/bundles/openai-codex-pr-26734.json new file mode 100644 index 000000000..2c6eae765 --- /dev/null +++ b/artifacts/github/bundles/openai-codex-pr-26734.json @@ -0,0 +1,232 @@ +{ + "analysis_mode": "pr_first", + "commits": [ + { + "author": "pakrym-oai", + "committed_at": "2026-06-06T04:05:34Z", + "message": "Handle Ctrl-C for non-TTY unified exec", + "sha": "b564da44fb798e9e8e5c89ff912ab72a70a2ba9c", + "url": "https://github.com/openai/codex/commit/b564da44fb798e9e8e5c89ff912ab72a70a2ba9c" + }, + { + "author": "pakrym-oai", + "committed_at": "2026-06-08T21:35:29Z", + "message": "Report unsupported process interrupts", + "sha": "5b5caa9431c818837eadc6d01cdd64e3b597ce9d", + "url": "https://github.com/openai/codex/commit/5b5caa9431c818837eadc6d01cdd64e3b597ce9d" + }, + { + "author": "pakrym-oai", + "committed_at": "2026-06-08T22:00:38Z", + "message": "Avoid expect_err in exec server test", + "sha": "83c0b87e952501c98f96957c3df2b2efa45352f7", + "url": "https://github.com/openai/codex/commit/83c0b87e952501c98f96957c3df2b2efa45352f7" + }, + { + "author": "pakrym-oai", + "committed_at": "2026-06-09T15:23:10Z", + "message": "Report SIGINT exit code for unified exec", + "sha": "9ff7581214ba17137fd351893766e6be3612ff05", + "url": "https://github.com/openai/codex/commit/9ff7581214ba17137fd351893766e6be3612ff05" + }, + { + "author": "pakrym-oai", + "committed_at": "2026-06-09T16:59:29Z", + "message": "codex: fix CI failure on PR #26734", + "sha": "e605b381502ef535c95f6e57798cc78a08703912", + "url": "https://github.com/openai/codex/commit/e605b381502ef535c95f6e57798cc78a08703912" + }, + { + "author": "pakrym-oai", + "committed_at": "2026-06-09T18:53:52Z", + "message": "Merge remote-tracking branch 'origin/main' into pakrym/unified-exec-non-tty-interrupt", + "sha": "eb4878b26439e36e1446fc72da6a8b25b495bfd1", + "url": "https://github.com/openai/codex/commit/eb4878b26439e36e1446fc72da6a8b25b495bfd1" + } + ], + "default_branch": "main", + "docs_refs": [], + "examples_refs": [], + "extracted_flags": [ + "SIGINT", + "INT", + "UNIFIED_EXEC_OUTPUT_MAX_TOKENS", + "EARLY_EXIT_GRACE_PERIOD", + "UNIFIED_EXEC_ENV", + "NETWORK_ACCESS_DENIED_MESSAGE", + "LATE_NETWORK_DENIAL_GRACE_PERIOD", + "INTERRUPT", + "READY", + "POST", + "NUL", + "EXEC_EXITED_METHOD", + "EXEC_METHOD", + "EXEC_OUTPUT_DELTA_METHOD", + "EXEC_READ_METHOD", + "EXEC_SIGNAL_METHOD", + "EXEC_TERMINATE_METHOD", + "EXEC_WRITE_METHOD", + "INITIALIZED_METHOD", + "JSONRPCE", + "MAX", + "ENVIRONMENT_INFO_METHOD", + "INITIALIZE_METHOD", + "CLI", + "PTY", + "SIGTERM", + "ESRCH", + "SIGKILL" + ], + "files": [ + { + "additions": 14, + "deletions": 1, + "patch_excerpt": "@@ -14,6 +14,7 @@ use tokio_util::sync::CancellationToken;\n \n use crate::exec::is_likely_sandbox_denied;\n use codex_exec_server::ExecProcess;\n+use codex_exec_server::ProcessSignal as ExecServerProcessSignal;\n use codex_exec_server::ReadResponse as ExecReadResponse;\n use codex_exec_server::StartedExecProcess;\n use codex_exec_server::WriteStatus;\n@@ -23,6 +24,7 @@ use codex_protocol::protocol::TruncationPolicy;\n use codex_sandboxing::SandboxType;\n use codex_utils_output_truncation::formatted_truncate_text;\n use codex_utils_pty::ExecCommandSession;\n+use codex_utils_pty::ProcessSignal as PtyProcessSignal;\n use codex_utils_pty::SpawnedPty;\n \n use super::UNIFIED_EXEC_OUTPUT_MAX_TOKENS;\n@@ -31,7 +33,6 @@ use super::head_tail_buffer::HeadTailBuffer;\n use super::process_state::ProcessState;\n \n const EARLY_EXIT_GRACE_PERIOD: Duration = Duration::from_millis(150);\n-\n pub(crate) trait SpawnLifecycle...", + "path": "codex-rs/core/src/unified_exec/process.rs", + "status": "modified" + }, + { + "additions": 23, + "deletions": 17, + "patch_excerpt": "@@ -72,6 +72,7 @@ const UNIFIED_EXEC_ENV: [(&str, &str); 10] = [\n const NETWORK_ACCESS_DENIED_MESSAGE: &str =\n \"Network access was denied by the Codex sandbox network proxy.\";\n const LATE_NETWORK_DENIAL_GRACE_PERIOD: Duration = Duration::from_millis(100);\n+const INTERRUPT: &str = \"\\u{3}\";\n \n /// Test-only override for deterministic unified exec process IDs.\n ///\n@@ -617,24 +618,29 @@ impl UnifiedExecProcessManager {\n \n if !request.input.is_empty() {\n if !tty {\n- return Err(UnifiedExecError::StdinClosed);\n- }\n- match process.write(request.input.as_bytes()).await {\n- Ok(()) => {\n- // Give the remote process a brief window to react so that we are\n- // more likely to capture its output in the poll below.\n- tokio::time::sleep(Duration::from_millis(100)).await;\n+...", + "path": "codex-rs/core/src/unified_exec/process_manager.rs", + "status": "modified" + }, + { + "additions": 5, + "deletions": 0, + "patch_excerpt": "@@ -5,6 +5,7 @@ use codex_exec_server::ExecProcess;\n use codex_exec_server::ExecProcessEventReceiver;\n use codex_exec_server::ExecServerError;\n use codex_exec_server::ProcessId;\n+use codex_exec_server::ProcessSignal;\n use codex_exec_server::ReadResponse;\n use codex_exec_server::StartedExecProcess;\n use codex_exec_server::WriteResponse;\n@@ -63,6 +64,10 @@ impl ExecProcess for MockExecProcess {\n Ok(self.write_response.clone())\n }\n \n+ async fn signal(&self, _signal: ProcessSignal) -> Result<(), ExecServerError> {\n+ Ok(())\n+ }\n+\n async fn terminate(&self) -> Result<(), ExecServerError> {\n Ok(())\n }", + "path": "codex-rs/core/src/unified_exec/process_tests.rs", + "status": "modified" + }, + { + "additions": 243, + "deletions": 0, + "patch_excerpt": "@@ -1995,6 +1995,249 @@ async fn write_stdin_returns_exit_metadata_and_clears_session() -> Result<()> {\n Ok(())\n }\n \n+#[tokio::test(flavor = \"multi_thread\", worker_threads = 2)]\n+async fn write_stdin_ctrl_c_interrupts_non_tty_session() -> Result<()> {\n+ assert_write_stdin_ctrl_c_interrupts_non_tty_session(\n+ \"trap\",\n+ \"trap 'echo INT-TRAP; exit 42' INT; echo READY; while true; do sleep 30; done\",\n+ /*expected_exit_code*/ 42,\n+ Some(\"INT-TRAP\"),\n+ )\n+ .await\n+}\n+\n+#[tokio::test(flavor = \"multi_thread\", worker_threads = 2)]\n+async fn write_stdin_ctrl_c_default_interrupt_reports_130_for_non_tty_session() -> Result<()> {\n+ assert_write_stdin_ctrl_c_interrupts_non_tty_session(\n+ \"default\",\n+ \"echo READY; exec sleep 30\",\n+ /*expected_exit_code*/ 130,\n+ /*expected_interrupt_output*/ None,\n+ )\n+ .await\n+}\n+\n+async fn a...", + "path": "codex-rs/core/tests/suite/unified_exec.rs", + "status": "modified" + }, + { + "additions": 25, + "deletions": 0, + "patch_excerpt": "@@ -35,6 +35,7 @@ use crate::protocol::EXEC_EXITED_METHOD;\n use crate::protocol::EXEC_METHOD;\n use crate::protocol::EXEC_OUTPUT_DELTA_METHOD;\n use crate::protocol::EXEC_READ_METHOD;\n+use crate::protocol::EXEC_SIGNAL_METHOD;\n use crate::protocol::EXEC_TERMINATE_METHOD;\n use crate::protocol::EXEC_WRITE_METHOD;\n use crate::protocol::EnvironmentInfo;\n@@ -80,8 +81,11 @@ use crate::protocol::INITIALIZED_METHOD;\n use crate::protocol::InitializeParams;\n use crate::protocol::InitializeResponse;\n use crate::protocol::ProcessOutputChunk;\n+use crate::protocol::ProcessSignal;\n use crate::protocol::ReadParams;\n use crate::protocol::ReadResponse;\n+use crate::protocol::SignalParams;\n+use crate::protocol::SignalResponse;\n use crate::protocol::TerminateParams;\n use crate::protocol::TerminateResponse;\n use crate::protocol::WriteParams;\n@@ -394,6 +398,23 @@ impl ExecServerClient {\n .await\n }\n \n+...", + "path": "codex-rs/exec-server/src/client.rs", + "status": "modified" + }, + { + "additions": 3, + "deletions": 0, + "patch_excerpt": "@@ -93,9 +93,12 @@ pub use protocol::HttpRequestResponse;\n pub use protocol::InitializeParams;\n pub use protocol::InitializeResponse;\n pub use protocol::ProcessOutputChunk;\n+pub use protocol::ProcessSignal;\n pub use protocol::ReadParams;\n pub use protocol::ReadResponse;\n pub use protocol::ShellInfo;\n+pub use protocol::SignalParams;\n+pub use protocol::SignalResponse;\n pub use protocol::TerminateParams;\n pub use protocol::TerminateResponse;\n pub use protocol::WriteParams;", + "path": "codex-rs/exec-server/src/lib.rs", + "status": "modified" + }, + { + "additions": 51, + "deletions": 3, + "patch_excerpt": "@@ -10,6 +10,7 @@ use codex_protocol::config_types::EnvironmentVariablePattern;\n use codex_protocol::config_types::ShellEnvironmentPolicy;\n use codex_protocol::shell_environment;\n use codex_utils_pty::ExecCommandSession;\n+use codex_utils_pty::ProcessSignal as PtyProcessSignal;\n use codex_utils_pty::TerminalSize;\n use tokio::sync::Mutex;\n use tokio::sync::Notify;\n@@ -33,8 +34,11 @@ use crate::protocol::ExecOutputStream;\n use crate::protocol::ExecParams;\n use crate::protocol::ExecResponse;\n use crate::protocol::ProcessOutputChunk;\n+use crate::protocol::ProcessSignal;\n use crate::protocol::ReadParams;\n use crate::protocol::ReadResponse;\n+use crate::protocol::SignalParams;\n+use crate::protocol::SignalResponse;\n use crate::protocol::TerminateParams;\n use crate::protocol::TerminateResponse;\n use crate::protocol::WriteParams;\n@@ -272,7 +276,6 @@ impl LocalProcess {\n &self,\n para...", + "path": "codex-rs/exec-server/src/local_process.rs", + "status": "modified" + }, + { + "additions": 3, + "deletions": 0, + "patch_excerpt": "@@ -10,6 +10,7 @@ use crate::ExecServerError;\n use crate::ProcessId;\n use crate::protocol::ExecParams;\n use crate::protocol::ProcessOutputChunk;\n+use crate::protocol::ProcessSignal;\n use crate::protocol::ReadResponse;\n use crate::protocol::WriteResponse;\n \n@@ -178,6 +179,8 @@ pub trait ExecProcess: Send + Sync {\n \n async fn write(&self, chunk: Vec) -> Result;\n \n+ async fn signal(&self, signal: ProcessSignal) -> Result<(), ExecServerError>;\n+\n async fn terminate(&self) -> Result<(), ExecServerError>;\n }", + "path": "codex-rs/exec-server/src/process.rs", + "status": "modified" + }, + { + "additions": 18, + "deletions": 0, + "patch_excerpt": "@@ -15,6 +15,7 @@ pub const INITIALIZED_METHOD: &str = \"initialized\";\n pub const EXEC_METHOD: &str = \"process/start\";\n pub const EXEC_READ_METHOD: &str = \"process/read\";\n pub const EXEC_WRITE_METHOD: &str = \"process/write\";\n+pub const EXEC_SIGNAL_METHOD: &str = \"process/signal\";\n pub const EXEC_TERMINATE_METHOD: &str = \"process/terminate\";\n pub const EXEC_OUTPUT_DELTA_METHOD: &str = \"process/output\";\n pub const EXEC_EXITED_METHOD: &str = \"process/exited\";\n@@ -166,6 +167,23 @@ pub struct WriteResponse {\n pub status: WriteStatus,\n }\n \n+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\n+#[serde(rename_all = \"camelCase\")]\n+pub enum ProcessSignal {\n+ Interrupt,\n+}\n+\n+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n+#[serde(rename_all = \"camelCase\")]\n+pub struct SignalParams {\n+ pub process_id: ProcessId,\n+ pub signal: ProcessSignal,\n+}\n+\n+#[deriv...", + "path": "codex-rs/exec-server/src/protocol.rs", + "status": "modified" + }, + { + "additions": 6, + "deletions": 0, + "patch_excerpt": "@@ -12,6 +12,7 @@ use crate::StartedExecProcess;\n use crate::client::LazyRemoteExecServerClient;\n use crate::client::Session;\n use crate::protocol::ExecParams;\n+use crate::protocol::ProcessSignal;\n use crate::protocol::ReadResponse;\n use crate::protocol::WriteResponse;\n \n@@ -76,6 +77,11 @@ impl ExecProcess for RemoteExecProcess {\n self.session.write(chunk).await\n }\n \n+ async fn signal(&self, signal: ProcessSignal) -> Result<(), ExecServerError> {\n+ trace!(\"exec process signal\");\n+ self.session.signal(signal).await\n+ }\n+\n async fn terminate(&self) -> Result<(), ExecServerError> {\n trace!(\"exec process terminate\");\n self.session.terminate().await", + "path": "codex-rs/exec-server/src/remote_process.rs", + "status": "modified" + }, + { + "additions": 10, + "deletions": 0, + "patch_excerpt": "@@ -42,6 +42,8 @@ use crate::protocol::InitializeParams;\n use crate::protocol::InitializeResponse;\n use crate::protocol::ReadParams;\n use crate::protocol::ReadResponse;\n+use crate::protocol::SignalParams;\n+use crate::protocol::SignalResponse;\n use crate::protocol::TerminateParams;\n use crate::protocol::TerminateResponse;\n use crate::protocol::WriteParams;\n@@ -171,6 +173,14 @@ impl ExecServerHandler {\n session.process().exec_write(params).await\n }\n \n+ pub(crate) async fn signal(\n+ &self,\n+ params: SignalParams,\n+ ) -> Result {\n+ let session = self.require_initialized_for(\"exec\")?;\n+ session.process().signal(params).await\n+ }\n+\n pub(crate) async fn terminate(\n &self,\n params: TerminateParams,", + "path": "codex-rs/exec-server/src/server/handler.rs", + "status": "modified" + }, + { + "additions": 9, + "deletions": 0, + "patch_excerpt": "@@ -5,6 +5,8 @@ use crate::protocol::ExecParams;\n use crate::protocol::ExecResponse;\n use crate::protocol::ReadParams;\n use crate::protocol::ReadResponse;\n+use crate::protocol::SignalParams;\n+use crate::protocol::SignalResponse;\n use crate::protocol::TerminateParams;\n use crate::protocol::TerminateResponse;\n use crate::protocol::WriteParams;\n@@ -49,6 +51,13 @@ impl ProcessHandler {\n self.process.exec_write(params).await\n }\n \n+ pub(crate) async fn signal(\n+ &self,\n+ params: SignalParams,\n+ ) -> Result {\n+ self.process.signal_process(params).await\n+ }\n+\n pub(crate) async fn terminate(\n &self,\n params: TerminateParams,", + "path": "codex-rs/exec-server/src/server/process_handler.rs", + "status": "modified" + }, + { + "additions": 8, + "deletions": 0, + "patch_excerpt": "@@ -3,6 +3,7 @@ use std::sync::Arc;\n use crate::protocol::ENVIRONMENT_INFO_METHOD;\n use crate::protocol::EXEC_METHOD;\n use crate::protocol::EXEC_READ_METHOD;\n+use crate::protocol::EXEC_SIGNAL_METHOD;\n use crate::protocol::EXEC_TERMINATE_METHOD;\n use crate::protocol::EXEC_WRITE_METHOD;\n use crate::protocol::ExecParams;\n@@ -32,6 +33,7 @@ use crate::protocol::INITIALIZE_METHOD;\n use crate::protocol::INITIALIZED_METHOD;\n use crate::protocol::InitializeParams;\n use crate::protocol::ReadParams;\n+use crate::protocol::SignalParams;\n use crate::protocol::TerminateParams;\n use crate::protocol::WriteParams;\n use crate::rpc::RpcRouter;\n@@ -77,6 +79,12 @@ pub(crate) fn build_router() -> RpcRouter {\n handler.exec_write(params).await\n },\n );\n+ router.request(\n+ EXEC_SIGNAL_METHOD,\n+ |handler: Arc, params: SignalParams| async...", + "path": "codex-rs/exec-server/src/server/registry.rs", + "status": "modified" + }, + { + "additions": 123, + "deletions": 2, + "patch_excerpt": "@@ -1,5 +1,3 @@\n-#![cfg(unix)]\n-\n mod common;\n \n use std::sync::Arc;\n@@ -13,6 +11,7 @@ use codex_exec_server::ExecParams;\n use codex_exec_server::ExecProcess;\n use codex_exec_server::ExecProcessEvent;\n use codex_exec_server::ProcessId;\n+use codex_exec_server::ProcessSignal;\n use codex_exec_server::ReadResponse;\n use codex_exec_server::StartedExecProcess;\n use codex_exec_server::WriteStatus;\n@@ -505,6 +504,98 @@ async fn assert_exec_process_rejects_write_without_pipe_stdin(use_remote: bool)\n Ok(())\n }\n \n+async fn assert_exec_process_signal_interrupts_process(use_remote: bool) -> Result<()> {\n+ let context = create_process_context(use_remote).await?;\n+ let process_id = \"proc-signal\".to_string();\n+ let session = context\n+ .backend\n+ .start(ExecParams {\n+ process_id: process_id.clone().into(),\n+ argv: vec![\n+ \"/bin/sh\".to_string...", + "path": "codex-rs/exec-server/tests/exec_process.rs", + "status": "modified" + }, + { + "additions": 2, + "deletions": 0, + "patch_excerpt": "@@ -17,6 +17,8 @@ pub use pipe::spawn_process_no_stdin as spawn_pipe_process_no_stdin;\n pub use process::ProcessDriver;\n /// Handle for interacting with a spawned process (PTY or pipe).\n pub use process::ProcessHandle;\n+/// Process signal supported by spawned-process handles.\n+pub use process::ProcessSignal;\n /// Bundle of process handles plus split output and exit receivers returned by spawn helpers.\n pub use process::SpawnedProcess;\n /// Terminal size in character cells used for PTY spawn and resize operations.", + "path": "codex-rs/utils/pty/src/lib.rs", + "status": "modified" + }, + { + "additions": 19, + "deletions": 1, + "patch_excerpt": "@@ -19,7 +19,9 @@ use tokio::task::JoinHandle;\n \n use crate::process::ChildTerminator;\n use crate::process::ProcessHandle;\n+use crate::process::ProcessSignal;\n use crate::process::SpawnedProcess;\n+use crate::process::exit_code_from_status;\n \n #[cfg(target_os = \"linux\")]\n use libc;\n@@ -32,6 +34,22 @@ struct PipeChildTerminator {\n }\n \n impl ChildTerminator for PipeChildTerminator {\n+ fn signal(&mut self, signal: ProcessSignal) -> io::Result<()> {\n+ match signal {\n+ ProcessSignal::Interrupt => {\n+ #[cfg(unix)]\n+ {\n+ crate::process_group::interrupt_process_group(self.process_group_id)\n+ }\n+\n+ #[cfg(not(unix))]\n+ {\n+ Err(crate::process::unsupported_signal(signal))\n+ }\n+ }\n+ }\n+ }\n+\n fn kill(&mut self) -> io::Result<()> {\n...", + "path": "codex-rs/utils/pty/src/pipe.rs", + "status": "modified" + }, + { + "additions": 48, + "deletions": 0, + "patch_excerpt": "@@ -2,6 +2,7 @@ use core::fmt;\n use std::io;\n #[cfg(unix)]\n use std::os::fd::RawFd;\n+use std::process::ExitStatus;\n use std::sync::Arc;\n use std::sync::Mutex as StdMutex;\n use std::sync::atomic::AtomicBool;\n@@ -17,7 +18,39 @@ use tokio::sync::watch;\n use tokio::task::AbortHandle;\n use tokio::task::JoinHandle;\n \n+#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n+pub enum ProcessSignal {\n+ Interrupt,\n+}\n+\n+pub(crate) fn unsupported_signal(signal: ProcessSignal) -> io::Error {\n+ match signal {\n+ ProcessSignal::Interrupt => io::Error::new(\n+ io::ErrorKind::Unsupported,\n+ \"process interrupt is not supported by this process backend\",\n+ ),\n+ }\n+}\n+\n+pub(crate) fn exit_code_from_status(status: ExitStatus) -> i32 {\n+ if let Some(code) = status.code() {\n+ return code;\n+ }\n+\n+ #[cfg(unix)]\n+ {\n+ use std::os::unix::process::ExitStatu...", + "path": "codex-rs/utils/pty/src/process.rs", + "status": "modified" + }, + { + "additions": 24, + "deletions": 19, + "patch_excerpt": "@@ -118,15 +118,10 @@ pub fn kill_process_group_by_pid(_pid: u32) -> io::Result<()> {\n }\n \n #[cfg(unix)]\n-/// Send SIGTERM to a specific process group ID (best-effort).\n-///\n-/// Returns `Ok(true)` when SIGTERM was delivered to an existing group and\n-/// `Ok(false)` when the group no longer exists.\n-pub fn terminate_process_group(process_group_id: u32) -> io::Result {\n+fn signal_process_group_id(pgid: libc::pid_t, signal: libc::c_int) -> io::Result {\n use std::io::ErrorKind;\n \n- let pgid = process_group_id as libc::pid_t;\n- let result = unsafe { libc::killpg(pgid, libc::SIGTERM) };\n+ let result = unsafe { libc::killpg(pgid, signal) };\n if result == -1 {\n let err = io::Error::last_os_error();\n if err.kind() == ErrorKind::NotFound || err.raw_os_error() == Some(libc::ESRCH) {\n@@ -138,29 +133,39 @@ pub fn terminate_process_group(process_group_id: ...", + "path": "codex-rs/utils/pty/src/process_group.rs", + "status": "modified" + }, + { + "additions": 25, + "deletions": 1, + "patch_excerpt": "@@ -30,10 +30,13 @@ use tokio::task::JoinHandle;\n \n use crate::process::ChildTerminator;\n use crate::process::ProcessHandle;\n+use crate::process::ProcessSignal;\n use crate::process::PtyHandles;\n use crate::process::PtyMasterHandle;\n use crate::process::SpawnedProcess;\n use crate::process::TerminalSize;\n+#[cfg(unix)]\n+use crate::process::exit_code_from_status;\n \n /// Returns true when ConPTY support is available (Windows only).\n #[cfg(windows)]\n@@ -54,6 +57,19 @@ struct PtyChildTerminator {\n }\n \n impl ChildTerminator for PtyChildTerminator {\n+ fn signal(&mut self, signal: ProcessSignal) -> std::io::Result<()> {\n+ match signal {\n+ ProcessSignal::Interrupt => {\n+ #[cfg(unix)]\n+ if let Some(process_group_id) = self.process_group_id {\n+ return crate::process_group::interrupt_process_group(process_group_id);\n+ ...", + "path": "codex-rs/utils/pty/src/pty.rs", + "status": "modified" + } + ], + "linked_issues": [ + "#26734" + ], + "notes": [ + "Built from GitHub pull-request, commits, files, and repo endpoints." + ], + "primary_pr": { + "body": "## Why\n\nA long-running unified exec process started with `tty: false` could not be interrupted via `write_stdin`: ordinary non-TTY stdin writes are rejected once stdin is closed, but an exact U+0003 payload should still map to a process interrupt. The interrupt should flow through the same process lifecycle path as a real signal so Codex preserves process-reported output and exit metadata instead of fabricating a Ctrl-C exit code or tearing down the session early.\n\n## What Changed\n\n- Add `process/signal` to exec-server with `ProcessSignal::Interrupt` and an empty response.\n- Add a non-consuming `ProcessHandle::signal` path for spawned processes; on Unix it sends SIGINT to the process group and leaves terminate/hard-kill unchanged.\n- Route non-TTY U+0003 `write_stdin` through `process.signal(...)` instead of `terminate`, then let the normal post-write collection path drain output and observe exit.\n- Add exec-server coverage where a shell `trap INT` handler prints the signal and exits with its own code.\n- Add unified exec coverage where a `tty: false` process traps SIGINT, emits output, and exits with its own code.\n\n## Validation\n\n- `just test -p codex-exec-server exec_process_signal_interrupts_process`\n- `just test -p codex-exec-server`\n- `just test -p codex-core write_stdin_ctrl_c_interrupts_non_tty_session`\n", + "labels": [], + "merged_at": "2026-06-09T22:10:18Z", + "number": 26734, + "state": "merged", + "title": "[codex] Handle Ctrl-C for non-TTY unified exec", + "url": "https://github.com/openai/codex/pull/26734" + }, + "repo": "openai/codex", + "schema": "github_change_bundle/v1" +} diff --git a/artifacts/github/bundles/openai-codex-pr-27129.json b/artifacts/github/bundles/openai-codex-pr-27129.json new file mode 100644 index 000000000..1bff07fcf --- /dev/null +++ b/artifacts/github/bundles/openai-codex-pr-27129.json @@ -0,0 +1,130 @@ +{ + "analysis_mode": "pr_first", + "commits": [ + { + "author": "celia-oai", + "committed_at": "2026-06-09T05:12:43Z", + "message": "Use provider defaults for memory models", + "sha": "00f1e544854c67163899cd0c6c3091c6d4a7694e", + "url": "https://github.com/openai/codex/commit/00f1e544854c67163899cd0c6c3091c6d4a7694e" + }, + { + "author": "celia-oai", + "committed_at": "2026-06-09T19:32:33Z", + "message": "comments", + "sha": "38b487f2a4eb2291eab6ccea41d24455acfbe9c0", + "url": "https://github.com/openai/codex/commit/38b487f2a4eb2291eab6ccea41d24455acfbe9c0" + }, + { + "author": "celia-oai", + "committed_at": "2026-06-09T21:52:24Z", + "message": "changes", + "sha": "b3dce407ff660c49d616be8920bc50a74ced8ac1", + "url": "https://github.com/openai/codex/commit/b3dce407ff660c49d616be8920bc50a74ced8ac1" + }, + { + "author": "celia-oai", + "committed_at": "2026-06-09T23:19:12Z", + "message": "changes", + "sha": "717e4b299bf0815a2fd1bd77d61ed5f3fbc7ae40", + "url": "https://github.com/openai/codex/commit/717e4b299bf0815a2fd1bd77d61ed5f3fbc7ae40" + } + ], + "default_branch": "main", + "docs_refs": [], + "examples_refs": [], + "extracted_flags": [ + "MODEL", + "REASONING_EFFORT", + "CONCURRENCY_LIMIT", + "JOB_LEASE_SECONDS", + "MOCK_PROVIDER_PHASE_ONE_MODEL", + "MOCK_PROVIDER_PHASE_TWO_MODEL", + "AMAZON_BEDROCK_GPT_5_4_MODEL_ID", + "DEFAULT_APPROVAL_REVIEW_PREFERRED_MODEL", + "DEFAULT_MEMORY_EXTRACTION_PREFERRED_MODEL", + "DEFAULT_MEMORY_CONSOLIDATION_PREFERRED_MODEL" + ], + "files": [ + { + "additions": 2, + "deletions": 0, + "patch_excerpt": "@@ -3320,6 +3320,8 @@ dependencies = [\n \"codex-features\",\n \"codex-git-utils\",\n \"codex-login\",\n+ \"codex-model-provider\",\n+ \"codex-model-provider-info\",\n \"codex-models-manager\",\n \"codex-otel\",\n \"codex-protocol\",", + "path": "codex-rs/Cargo.lock", + "status": "modified" + }, + { + "additions": 2, + "deletions": 0, + "patch_excerpt": "@@ -21,6 +21,7 @@ codex-config = { workspace = true }\n codex-features = { workspace = true }\n codex-git-utils = { workspace = true }\n codex-login = { workspace = true }\n+codex-model-provider = { workspace = true }\n codex-otel = { workspace = true }\n codex-protocol = { workspace = true }\n codex-rollout = { workspace = true }\n@@ -39,6 +40,7 @@ tracing = { workspace = true, features = [\"log\"] }\n uuid = { workspace = true, features = [\"v4\", \"v5\"] }\n \n [dev-dependencies]\n+codex-model-provider-info = { workspace = true }\n codex-models-manager = { workspace = true }\n core_test_support = { workspace = true }\n pretty_assertions = { workspace = true }", + "path": "codex-rs/memories/write/Cargo.toml", + "status": "modified" + }, + { + "additions": 0, + "deletions": 2, + "patch_excerpt": "@@ -76,7 +76,6 @@ signal to remove stale memories derived only from those resources.\n }\n \n mod stage_one {\n- pub(super) const MODEL: &str = \"gpt-5.4-mini\";\n pub(super) const REASONING_EFFORT: codex_protocol::openai_models::ReasoningEffort =\n codex_protocol::openai_models::ReasoningEffort::Low;\n pub(super) const CONCURRENCY_LIMIT: usize = 8;\n@@ -101,7 +100,6 @@ mod stage_one {\n }\n \n mod stage_two {\n- pub(super) const MODEL: &str = \"gpt-5.4\";\n pub(super) const REASONING_EFFORT: codex_protocol::openai_models::ReasoningEffort =\n codex_protocol::openai_models::ReasoningEffort::Medium;\n pub(super) const JOB_LEASE_SECONDS: i64 = 3_600;", + "path": "codex-rs/memories/write/src/lib.rs", + "status": "modified" + }, + { + "additions": 6, + "deletions": 5, + "patch_excerpt": "@@ -190,11 +190,12 @@ async fn build_request_context(\n context: &MemoryStartupContext,\n config: &Config,\n ) -> StageOneRequestContext {\n- let model_name = config\n- .memories\n- .extract_model\n- .clone()\n- .unwrap_or(crate::stage_one::MODEL.to_string());\n+ let model_name = config.memories.extract_model.clone().unwrap_or_else(|| {\n+ context\n+ .provider()\n+ .memory_extraction_preferred_model()\n+ .to_string()\n+ });\n context\n .stage_one_request_context(config, &model_name, crate::stage_one::REASONING_EFFORT)\n .await", + "path": "codex-rs/memories/write/src/phase1.rs", + "status": "modified" + }, + { + "additions": 4, + "deletions": 3, + "patch_excerpt": "@@ -16,6 +16,7 @@ use crate::workspace::write_workspace_diff;\n use codex_config::Constrained;\n use codex_core::config::Config;\n use codex_features::Feature;\n+use codex_model_provider::ModelProvider;\n use codex_protocol::ThreadId;\n use codex_protocol::protocol::AgentStatus;\n use codex_protocol::protocol::AskForApproval;\n@@ -76,7 +77,7 @@ pub async fn run(context: Arc, config: Arc) {\n }\n \n // 3. Build the locked-down config used by the consolidation agent.\n- let Some(agent_config) = agent::get_config(config.as_ref()) else {\n+ let Some(agent_config) = agent::get_config(config.as_ref(), context.provider()) else {\n // If we can't get the config, we can't consolidate.\n tracing::error!(\"failed to get agent config\");\n job::failed(\n@@ -297,7 +298,7 @@ mod agent {\n use super::*;\n use tracing::warn;\n \n- pub(super) fn get...", + "path": "codex-rs/memories/write/src/phase2.rs", + "status": "modified" + }, + { + "additions": 54, + "deletions": 0, + "patch_excerpt": "@@ -13,6 +13,9 @@ use codex_login::AuthManager;\n use codex_login::CodexAuth;\n use codex_login::auth_env_telemetry::collect_auth_env_telemetry;\n use codex_login::default_client::originator;\n+use codex_model_provider::ModelProvider;\n+use codex_model_provider::SharedModelProvider;\n+use codex_model_provider::create_model_provider;\n use codex_otel::SessionTelemetry;\n use codex_otel::TelemetryAuthMode;\n use codex_protocol::SessionId;\n@@ -68,6 +71,7 @@ pub(crate) struct MemoryStartupContext {\n thread: Arc,\n thread_manager: Arc,\n auth_manager: Arc,\n+ provider: SharedModelProvider,\n session_telemetry: SessionTelemetry,\n }\n \n@@ -79,6 +83,51 @@ impl MemoryStartupContext {\n thread: Arc,\n config: &Config,\n source: SessionSource,\n+ ) -> Self {\n+ let provider = create_model_provider(\n+ ...", + "path": "codex-rs/memories/write/src/runtime.rs", + "status": "modified" + }, + { + "additions": 315, + "deletions": 3, + "patch_excerpt": "@@ -1,12 +1,30 @@\n+use crate::extensions::seed_extension_instructions;\n+use crate::memory_root;\n+use crate::phase1;\n+use crate::phase2;\n+use crate::runtime::MemoryStartupContext;\n use crate::start_memories_startup_task;\n+use codex_config::types::MemoriesConfig;\n use codex_features::Feature;\n use codex_git_utils::diff_since_latest_init;\n use codex_git_utils::reset_git_repository;\n+use codex_login::AuthManager;\n+use codex_login::CodexAuth;\n+use codex_model_provider::ModelProvider;\n+use codex_model_provider::ProviderAccountResult;\n+use codex_model_provider::SharedModelProvider;\n+use codex_model_provider::create_model_provider;\n+use codex_model_provider_info::ModelProviderInfo;\n use codex_protocol::ThreadId;\n use codex_protocol::config_types::ServiceTier;\n+use codex_protocol::models::ContentItem;\n+use codex_protocol::models::ResponseItem;\n+use codex_protocol::openai_models::ModelsResponse;\n ...", + "path": "codex-rs/memories/write/src/startup_tests.rs", + "status": "modified" + }, + { + "additions": 8, + "deletions": 0, + "patch_excerpt": "@@ -68,6 +68,14 @@ impl ModelProvider for AmazonBedrockModelProvider {\n AMAZON_BEDROCK_GPT_5_4_MODEL_ID\n }\n \n+ fn memory_extraction_preferred_model(&self) -> &'static str {\n+ AMAZON_BEDROCK_GPT_5_4_MODEL_ID\n+ }\n+\n+ fn memory_consolidation_preferred_model(&self) -> &'static str {\n+ AMAZON_BEDROCK_GPT_5_4_MODEL_ID\n+ }\n+\n fn auth_manager(&self) -> Option> {\n None\n }", + "path": "codex-rs/model-provider/src/amazon_bedrock/mod.rs", + "status": "modified" + }, + { + "additions": 22, + "deletions": 0, + "patch_excerpt": "@@ -74,6 +74,14 @@ pub type ProviderAccountResult = std::result::Result