From 70a6d4b1b4171ec0d49220439937d8140720d4a1 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 5 Sep 2025 22:08:34 -0700 Subject: [PATCH 01/11] fix: change create_github_release to take either --publish-alpha or --publish-release (#3231) No more picking out version numbers by hand! Now we let the script do it: ``` $ ./codex-rs/scripts/create_github_release --dry-run --publish-alpha Running gh api GET /repos/openai/codex/releases/latest Running gh api GET /repos/openai/codex/releases?per_page=100 Publishing version 0.31.0-alpha.3 $ ./codex-rs/scripts/create_github_release --dry-run --publish-release Running gh api GET /repos/openai/codex/releases/latest Publishing version 0.31.0 ``` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/3230). * __->__ #3231 * #3230 * #3228 * #3226 --- codex-rs/scripts/create_github_release | 102 +++++++++++++++++++++++-- docs/release_management.md | 19 +++-- 2 files changed, 108 insertions(+), 13 deletions(-) diff --git a/codex-rs/scripts/create_github_release b/codex-rs/scripts/create_github_release index 08f0b11fc7..120e063541 100755 --- a/codex-rs/scripts/create_github_release +++ b/codex-rs/scripts/create_github_release @@ -14,10 +14,24 @@ CARGO_TOML_PATH = "codex-rs/Cargo.toml" def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Create a tagged Codex release.") + parser = argparse.ArgumentParser(description="Publish a tagged Codex release.") parser.add_argument( - "version", - help="Version string used for Cargo.toml and the Git tag (e.g. 0.1.0-alpha.4).", + "-n", + "--dry-run", + action="store_true", + help="Print the version that would be used and exit before making changes.", + ) + + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + "--publish-alpha", + action="store_true", + help="Publish the next alpha release for the upcoming minor version.", + ) + group.add_argument( + "--publish-release", + action="store_true", + help="Publish the next stable release by bumping the minor version.", ) return parser.parse_args(argv[1:]) @@ -25,6 +39,11 @@ def parse_args(argv: list[str]) -> argparse.Namespace: def main(argv: list[str]) -> int: args = parse_args(argv) try: + version = determine_version(args) + print(f"Publishing version {version}") + if args.dry_run: + return 0 + print("Fetching branch head...") base_commit = get_branch_head() print(f"Base commit: {base_commit}") @@ -34,7 +53,7 @@ def main(argv: list[str]) -> int: print("Fetching Cargo.toml...") current_contents = fetch_file_contents(base_commit) print("Updating version...") - updated_contents = replace_version(current_contents, args.version) + updated_contents = replace_version(current_contents, version) print("Creating blob...") blob_sha = create_blob(updated_contents) print(f"Blob SHA: {blob_sha}") @@ -42,13 +61,13 @@ def main(argv: list[str]) -> int: tree_sha = create_tree(base_tree, blob_sha) print(f"Tree SHA: {tree_sha}") print("Creating commit...") - commit_sha = create_commit(args.version, tree_sha, base_commit) + commit_sha = create_commit(version, tree_sha, base_commit) print(f"Commit SHA: {commit_sha}") print("Creating tag...") - tag_sha = create_tag(args.version, commit_sha) + tag_sha = create_tag(version, commit_sha) print(f"Tag SHA: {tag_sha}") print("Creating tag ref...") - create_tag_ref(args.version, tag_sha) + create_tag_ref(version, tag_sha) print("Done.") except ReleaseError as error: print(f"ERROR: {error}", file=sys.stderr) @@ -59,6 +78,7 @@ def main(argv: list[str]) -> int: class ReleaseError(RuntimeError): pass + def run_gh_api(endpoint: str, *, method: str = "GET", payload: dict | None = None) -> dict: print(f"Running gh api {method} {endpoint}") command = [ @@ -204,5 +224,73 @@ def create_tag_ref(version: str, tag_sha: str) -> None: ) +def determine_version(args: argparse.Namespace) -> str: + latest_version = get_latest_release_version() + major, minor, patch = parse_semver(latest_version) + next_minor_version = format_version(major, minor + 1, patch) + + if args.publish_release: + return next_minor_version + + alpha_prefix = f"{next_minor_version}-alpha." + releases = list_releases() + highest_alpha = 0 + found_alpha = False + for release in releases: + tag = release.get("tag_name", "") + candidate = strip_tag_prefix(tag) + if candidate and candidate.startswith(alpha_prefix): + suffix = candidate[len(alpha_prefix) :] + try: + alpha_number = int(suffix) + except ValueError: + continue + highest_alpha = max(highest_alpha, alpha_number) + found_alpha = True + + if found_alpha: + return f"{alpha_prefix}{highest_alpha + 1}" + return f"{alpha_prefix}1" + + +def get_latest_release_version() -> str: + response = run_gh_api(f"/repos/{REPO}/releases/latest") + tag = response.get("tag_name") + version = strip_tag_prefix(tag) + if not version: + raise ReleaseError("Latest release tag has unexpected format.") + return version + + +def list_releases() -> list[dict]: + response = run_gh_api(f"/repos/{REPO}/releases?per_page=100") + if not isinstance(response, list): + raise ReleaseError("Unexpected response when listing releases.") + return response + + +def strip_tag_prefix(tag: str | None) -> str | None: + if not tag: + return None + prefix = "rust-v" + if not tag.startswith(prefix): + return None + return tag[len(prefix) :] + + +def parse_semver(version: str) -> tuple[int, int, int]: + parts = version.split(".") + if len(parts) != 3: + raise ReleaseError(f"Unexpected version format: {version}") + try: + return int(parts[0]), int(parts[1]), int(parts[2]) + except ValueError as error: + raise ReleaseError(f"Version components must be integers: {version}") from error + + +def format_version(major: int, minor: int, patch: int) -> str: + return f"{major}.{minor}.{patch}" + + if __name__ == "__main__": sys.exit(main(sys.argv)) diff --git a/docs/release_management.md b/docs/release_management.md index 1b81bc3eb2..ed12de6e4a 100644 --- a/docs/release_management.md +++ b/docs/release_management.md @@ -8,16 +8,23 @@ Currently, we made Codex binaries available in three places: # Cutting a Release -Currently, choosing the version number for the next release is a manual process. In general, just go to https://github.com/openai/codex/releases/latest and see what the latest release is and increase the minor version by `1`, so if the current release is `0.20.0`, then the next release should be `0.21.0`. +Run the `codex-rs/scripts/create_github_release` script in the repository to publish a new release. The script will choose the appropriate version number depending on the type of release you are creating. -Assuming you are trying to publish `0.21.0`, first you would run: +To cut a new alpha release from `main` (feel free to cut alphas liberally): -```shell -VERSION=0.21.0 -./codex-rs/scripts/create_github_release.sh "$VERSION" +``` +./codex-rs/scripts/create_github_release --publish-alpha ``` -This will kick off a GitHub Action to build the release, so go to https://github.com/openai/codex/actions/workflows/rust-release.yml to find the corresponding workflow. (Note: we should automate finding the workflow URL with `gh`.) +To cut a new _public_ release from `main` (which requires more caution), run: + +``` +./codex-rs/scripts/create_github_release --publish-release +``` + +TIP: Add the `--dry-run` flag to report the next version number for the respective release and exit. + +Running the publishing script will kick off a GitHub Action to build the release, so go to https://github.com/openai/codex/actions/workflows/rust-release.yml to find the corresponding workflow. (Note: we should automate finding the workflow URL with `gh`.) When the workflow finishes, the GitHub Release is "done," but you still have to consider npm and Homebrew. From 0269096229e8c8bd95185173706807dc10838c7a Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Sat, 6 Sep 2025 08:19:23 -0700 Subject: [PATCH 02/11] Move token usage/context information to session level (#3221) Move context information into the main loop so it can be used to interrupt the loop or start auto-compaction. --- codex-rs/core/src/client.rs | 10 ++- codex-rs/core/src/codex.rs | 44 +++++++---- .../src/event_processor_with_human_output.rs | 10 ++- codex-rs/protocol/src/protocol.rs | 74 +++++++++++++++--- codex-rs/tui/src/bottom_pane/chat_composer.rs | 34 +++------ codex-rs/tui/src/bottom_pane/mod.rs | 12 +-- codex-rs/tui/src/chatwidget.rs | 76 ++++++------------- codex-rs/tui/src/chatwidget/tests.rs | 3 +- codex-rs/tui/src/history_cell.rs | 5 +- 9 files changed, 151 insertions(+), 117 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 48e42ee034..52c034ebba 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -398,9 +398,15 @@ impl From for TokenUsage { fn from(val: ResponseCompletedUsage) -> Self { TokenUsage { input_tokens: val.input_tokens, - cached_input_tokens: val.input_tokens_details.map(|d| d.cached_tokens), + cached_input_tokens: val + .input_tokens_details + .map(|d| d.cached_tokens) + .unwrap_or(0), output_tokens: val.output_tokens, - reasoning_output_tokens: val.output_tokens_details.map(|d| d.reasoning_tokens), + reasoning_output_tokens: val + .output_tokens_details + .map(|d| d.reasoning_tokens) + .unwrap_or(0), total_tokens: val.total_tokens, } } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 172d4e0ff7..95e9a96951 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -99,6 +99,7 @@ use crate::protocol::SessionConfiguredEvent; use crate::protocol::StreamErrorEvent; use crate::protocol::Submission; use crate::protocol::TaskCompleteEvent; +use crate::protocol::TokenUsageInfo; use crate::protocol::TurnDiffEvent; use crate::protocol::WebSearchBeginEvent; use crate::rollout::RolloutRecorder; @@ -261,6 +262,7 @@ struct State { pending_approvals: HashMap>, pending_input: Vec, history: ConversationHistory, + token_info: Option, } /// Context for an initialized model agent @@ -1767,15 +1769,23 @@ async fn try_run_turn( response_id: _, token_usage, } => { - if let Some(token_usage) = token_usage { - sess.tx_event - .send(Event { - id: sub_id.to_string(), - msg: EventMsg::TokenCount(token_usage), - }) - .await - .ok(); - } + let info = { + let mut st = sess.state.lock_unchecked(); + let info = TokenUsageInfo::new_or_append( + &st.token_info, + &token_usage, + turn_context.client.get_model_context_window(), + ); + st.token_info = info.clone(); + info + }; + sess.tx_event + .send(Event { + id: sub_id.to_string(), + msg: EventMsg::TokenCount(crate::protocol::TokenCountEvent { info }), + }) + .await + .ok(); let unified_diff = turn_diff_tracker.get_unified_diff(); if let Ok(Some(unified_diff)) = unified_diff { @@ -2841,13 +2851,21 @@ async fn drain_to_completed( response_id: _, token_usage, }) => { - // some providers don't return token usage, so we default - // TODO: consider approximate token usage - let token_usage = token_usage.unwrap_or_default(); + let info = { + let mut st = sess.state.lock_unchecked(); + let info = TokenUsageInfo::new_or_append( + &st.token_info, + &token_usage, + turn_context.client.get_model_context_window(), + ); + st.token_info = info.clone(); + info + }; + sess.tx_event .send(Event { id: sub_id.to_string(), - msg: EventMsg::TokenCount(token_usage), + msg: EventMsg::TokenCount(crate::protocol::TokenCountEvent { info }), }) .await .ok(); diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 0ee60b93ff..3c639ad25a 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -189,8 +189,14 @@ impl EventProcessor for EventProcessorWithHumanOutput { } return CodexStatus::InitiateShutdown; } - EventMsg::TokenCount(token_usage) => { - ts_println!(self, "tokens used: {}", token_usage.blended_total()); + EventMsg::TokenCount(ev) => { + if let Some(usage_info) = ev.info { + ts_println!( + self, + "tokens used: {}", + usage_info.total_token_usage.blended_total() + ); + } } EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }) => { if !self.answer_started { diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 2308b7d212..a422327db3 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -417,9 +417,9 @@ pub enum EventMsg { /// Agent has completed all actions TaskComplete(TaskCompleteEvent), - /// Token count event, sent periodically to report the number of tokens - /// used in the current session. - TokenCount(TokenUsage), + /// Usage update for the current session, including totals and last turn. + /// Optional means unknown — UIs should not display when `None`. + TokenCount(TokenCountEvent), /// Agent text output message AgentMessage(AgentMessageEvent), @@ -521,12 +521,54 @@ pub struct TaskStartedEvent { #[derive(Debug, Clone, Deserialize, Serialize, Default)] pub struct TokenUsage { pub input_tokens: u64, - pub cached_input_tokens: Option, + pub cached_input_tokens: u64, pub output_tokens: u64, - pub reasoning_output_tokens: Option, + pub reasoning_output_tokens: u64, pub total_tokens: u64, } +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TokenUsageInfo { + pub total_token_usage: TokenUsage, + pub last_token_usage: TokenUsage, + pub model_context_window: Option, +} + +impl TokenUsageInfo { + pub fn new_or_append( + info: &Option, + last: &Option, + model_context_window: Option, + ) -> Option { + if info.is_none() && last.is_none() { + return None; + } + + let mut info = match info { + Some(info) => info.clone(), + None => Self { + total_token_usage: TokenUsage::default(), + last_token_usage: TokenUsage::default(), + model_context_window, + }, + }; + if let Some(last) = last { + info.append_last_usage(last); + } + Some(info) + } + + pub fn append_last_usage(&mut self, last: &TokenUsage) { + self.total_token_usage.add_assign(last); + self.last_token_usage = last.clone(); + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TokenCountEvent { + pub info: Option, +} + // Includes prompts, tools and space to call compact. const BASELINE_TOKENS: u64 = 12000; @@ -536,7 +578,7 @@ impl TokenUsage { } pub fn cached_input(&self) -> u64 { - self.cached_input_tokens.unwrap_or(0) + self.cached_input_tokens } pub fn non_cached_input(&self) -> u64 { @@ -554,7 +596,7 @@ impl TokenUsage { /// This will be off for the current turn and pending function calls. pub fn tokens_in_context_window(&self) -> u64 { self.total_tokens - .saturating_sub(self.reasoning_output_tokens.unwrap_or(0)) + .saturating_sub(self.reasoning_output_tokens) } /// Estimate the remaining user-controllable percentage of the model's context window. @@ -579,6 +621,15 @@ impl TokenUsage { let remaining = effective_window.saturating_sub(used); ((remaining as f32 / effective_window as f32) * 100.0).clamp(0.0, 100.0) as u8 } + + /// In-place element-wise sum of token counts. + pub fn add_assign(&mut self, other: &TokenUsage) { + self.input_tokens += other.input_tokens; + self.cached_input_tokens += other.cached_input_tokens; + self.output_tokens += other.output_tokens; + self.reasoning_output_tokens += other.reasoning_output_tokens; + self.total_tokens += other.total_tokens; + } } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -606,10 +657,11 @@ impl fmt::Display for FinalOutput { String::new() }, token_usage.output_tokens, - token_usage - .reasoning_output_tokens - .map(|r| format!(" (reasoning {r})")) - .unwrap_or_default() + if token_usage.reasoning_output_tokens > 0 { + format!(" (reasoning {})", token_usage.reasoning_output_tokens) + } else { + String::new() + } ) } } diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 528290fa03..aead743d99 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -1,4 +1,4 @@ -use codex_core::protocol::TokenUsage; +use codex_core::protocol::TokenUsageInfo; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; @@ -63,12 +63,6 @@ struct AttachedImage { path: PathBuf, } -struct TokenUsageInfo { - total_token_usage: TokenUsage, - last_token_usage: TokenUsage, - model_context_window: Option, -} - pub(crate) struct ChatComposer { textarea: TextArea, textarea_state: RefCell, @@ -166,17 +160,8 @@ impl ChatComposer { /// Update the cached *context-left* percentage and refresh the placeholder /// text. The UI relies on the placeholder to convey the remaining /// context when the composer is empty. - pub(crate) fn set_token_usage( - &mut self, - total_token_usage: TokenUsage, - last_token_usage: TokenUsage, - model_context_window: Option, - ) { - self.token_usage_info = Some(TokenUsageInfo { - total_token_usage, - last_token_usage, - model_context_window, - }); + pub(crate) fn set_token_usage(&mut self, token_info: Option) { + self.token_usage_info = token_info; } /// Record the history metadata advertised by `SessionConfiguredEvent` so @@ -1290,11 +1275,16 @@ impl WidgetRef for ChatComposer { } else { 100 }; + let context_style = if percent_remaining < 20 { + Style::default().fg(Color::Yellow) + } else { + Style::default().add_modifier(Modifier::DIM) + }; hint.push(" ".into()); - hint.push( - Span::from(format!("{percent_remaining}% context left")) - .style(Style::default().add_modifier(Modifier::DIM)), - ); + hint.push(Span::styled( + format!("{percent_remaining}% context left"), + context_style, + )); } } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index d5daea2ebe..88b3f09646 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -5,7 +5,7 @@ use crate::app_event_sender::AppEventSender; use crate::tui::FrameRequester; use crate::user_approval_widget::ApprovalRequest; use bottom_pane_view::BottomPaneView; -use codex_core::protocol::TokenUsage; +use codex_core::protocol::TokenUsageInfo; use codex_file_search::FileMatch; use crossterm::event::KeyEvent; use ratatui::buffer::Buffer; @@ -358,14 +358,8 @@ impl BottomPane { /// Update the *context-window remaining* indicator in the composer. This /// is forwarded directly to the underlying `ChatComposer`. - pub(crate) fn set_token_usage( - &mut self, - total_token_usage: TokenUsage, - last_token_usage: TokenUsage, - model_context_window: Option, - ) { - self.composer - .set_token_usage(total_token_usage, last_token_usage, model_context_window); + pub(crate) fn set_token_usage(&mut self, token_info: Option) { + self.composer.set_token_usage(token_info); self.request_redraw(); } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 65ecbf749f..bee9d5e175 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -29,6 +29,7 @@ use codex_core::protocol::PatchApplyBeginEvent; use codex_core::protocol::StreamErrorEvent; use codex_core::protocol::TaskCompleteEvent; use codex_core::protocol::TokenUsage; +use codex_core::protocol::TokenUsageInfo; use codex_core::protocol::TurnAbortReason; use codex_core::protocol::TurnDiffEvent; use codex_core::protocol::UserMessageEvent; @@ -109,8 +110,7 @@ pub(crate) struct ChatWidget { active_exec_cell: Option, config: Config, initial_user_message: Option, - total_token_usage: TokenUsage, - last_token_usage: TokenUsage, + token_info: Option, // Stream lifecycle controller stream: StreamController, running_commands: HashMap, @@ -259,16 +259,10 @@ impl ChatWidget { self.maybe_send_next_queued_input(); } - fn on_token_count(&mut self, token_usage: TokenUsage) { - self.total_token_usage = add_token_usage(&self.total_token_usage, &token_usage); - self.last_token_usage = token_usage; - self.bottom_pane.set_token_usage( - self.total_token_usage.clone(), - self.last_token_usage.clone(), - self.config.model_context_window, - ); + pub(crate) fn set_token_info(&mut self, info: Option) { + self.bottom_pane.set_token_usage(info.clone()); + self.token_info = info; } - /// Finalize any active exec as failed, push an error message into history, /// and stop/clear running UI state. fn finalize_turn_with_error_message(&mut self, message: String) { @@ -659,8 +653,7 @@ impl ChatWidget { initial_prompt.unwrap_or_default(), initial_images, ), - total_token_usage: TokenUsage::default(), - last_token_usage: TokenUsage::default(), + token_info: None, stream: StreamController::new(config), running_commands: HashMap::new(), task_complete_pending: false, @@ -712,8 +705,7 @@ impl ChatWidget { initial_prompt.unwrap_or_default(), initial_images, ), - total_token_usage: TokenUsage::default(), - last_token_usage: TokenUsage::default(), + token_info: None, stream: StreamController::new(config), running_commands: HashMap::new(), task_complete_pending: false, @@ -1050,7 +1042,7 @@ impl ChatWidget { EventMsg::AgentReasoningSectionBreak(_) => self.on_reasoning_section_break(), EventMsg::TaskStarted(_) => self.on_task_started(), EventMsg::TaskComplete(TaskCompleteEvent { .. }) => self.on_task_complete(), - EventMsg::TokenCount(token_usage) => self.on_token_count(token_usage), + EventMsg::TokenCount(ev) => self.set_token_info(ev.info), EventMsg::Error(ErrorEvent { message }) => self.on_error(message), EventMsg::TurnAborted(ev) => match ev.reason { TurnAbortReason::Interrupted => { @@ -1157,9 +1149,16 @@ impl ChatWidget { } pub(crate) fn add_status_output(&mut self) { + let default_usage; + let usage_ref = if let Some(ti) = &self.token_info { + &ti.total_token_usage + } else { + default_usage = TokenUsage::default(); + &default_usage + }; self.add_to_history(history_cell::new_status_output( &self.config, - &self.total_token_usage, + usage_ref, &self.session_id, )); } @@ -1352,8 +1351,11 @@ impl ChatWidget { self.submit_user_message(text.into()); } - pub(crate) fn token_usage(&self) -> &TokenUsage { - &self.total_token_usage + pub(crate) fn token_usage(&self) -> TokenUsage { + self.token_info + .as_ref() + .map(|ti| ti.total_token_usage.clone()) + .unwrap_or_default() } pub(crate) fn session_id(&self) -> Option { @@ -1367,12 +1369,8 @@ impl ChatWidget { } pub(crate) fn clear_token_usage(&mut self) { - self.total_token_usage = TokenUsage::default(); - self.bottom_pane.set_token_usage( - self.total_token_usage.clone(), - self.last_token_usage.clone(), - self.config.model_context_window, - ); + self.token_info = None; + self.bottom_pane.set_token_usage(None); } pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { @@ -1405,34 +1403,6 @@ const EXAMPLE_PROMPTS: [&str; 6] = [ "Improve documentation in @filename", ]; -fn add_token_usage(current_usage: &TokenUsage, new_usage: &TokenUsage) -> TokenUsage { - let cached_input_tokens = match ( - current_usage.cached_input_tokens, - new_usage.cached_input_tokens, - ) { - (Some(current), Some(new)) => Some(current + new), - (Some(current), None) => Some(current), - (None, Some(new)) => Some(new), - (None, None) => None, - }; - let reasoning_output_tokens = match ( - current_usage.reasoning_output_tokens, - new_usage.reasoning_output_tokens, - ) { - (Some(current), Some(new)) => Some(current + new), - (Some(current), None) => Some(current), - (None, Some(new)) => Some(new), - (None, None) => None, - }; - TokenUsage { - input_tokens: current_usage.input_tokens + new_usage.input_tokens, - cached_input_tokens, - output_tokens: current_usage.output_tokens + new_usage.output_tokens, - reasoning_output_tokens, - total_tokens: current_usage.total_tokens + new_usage.total_tokens, - } -} - // Extract the first bold (Markdown) element in the form **...** from `s`. // Returns the inner text if found; otherwise `None`. fn extract_first_bold(s: &str) -> Option { diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 555ce50dbc..dfcbbec7d0 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -221,8 +221,7 @@ fn make_chatwidget_manual() -> ( active_exec_cell: None, config: cfg.clone(), initial_user_message: None, - total_token_usage: TokenUsage::default(), - last_token_usage: TokenUsage::default(), + token_info: None, stream: StreamController::new(cfg), running_commands: HashMap::new(), task_complete_pending: false, diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 7e1cf25e92..c8fb59232a 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -966,9 +966,8 @@ pub(crate) fn new_status_output( " • Input: ".into(), usage.non_cached_input().to_string().into(), ]; - if let Some(cached) = usage.cached_input_tokens - && cached > 0 - { + if usage.cached_input_tokens > 0 { + let cached = usage.cached_input_tokens; input_line_spans.push(format!(" (+ {cached} cached)").into()); } lines.push(Line::from(input_line_spans)); From 58d77ca4e7324cf442e40ef87024631c9206e001 Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Sun, 7 Sep 2025 20:21:53 -0700 Subject: [PATCH 03/11] Clear non-empty prompts with ctrl + c (#3285) This updates the ctrl + c behavior to clear the current prompt if there is text and you press ctrl + c. I also updated the ctrl + c hint text to show `^c to interrupt` instead of `^c to quit` if there is an active conversation. Two things I don't love: 1. You can currently interrupt a conversation with escape or ctrl + c (not related to this PR and maybe fine) 2. The bottom row hint text always says `^c to quit` but this PR doesn't really make that worse. https://github.com/user-attachments/assets/6eddadec-0d84-4fa7-abcb-d6f5a04e5748 Fixes https://github.com/openai/codex/issues/3126 --- .../tui/src/bottom_pane/bottom_pane_view.rs | 2 +- codex-rs/tui/src/bottom_pane/chat_composer.rs | 13 +++++++- codex-rs/tui/src/bottom_pane/mod.rs | 32 ++++++++++++------- codex-rs/tui/src/chatwidget.rs | 18 ++++++----- 4 files changed, 44 insertions(+), 21 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs b/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs index 8f21a242e4..794dd8c422 100644 --- a/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs +++ b/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs @@ -19,7 +19,7 @@ pub(crate) trait BottomPaneView { /// Handle Ctrl-C while this view is active. fn on_ctrl_c(&mut self, _pane: &mut BottomPane) -> CancellationEvent { - CancellationEvent::Ignored + CancellationEvent::NotHandled } /// Return the desired height of the view. diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index aead743d99..a8dd56b5d5 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -79,6 +79,7 @@ pub(crate) struct ChatComposer { has_focus: bool, attached_images: Vec, placeholder_text: String, + is_task_running: bool, // Non-bracketed paste burst tracker. paste_burst: PasteBurst, // When true, disables paste-burst logic and inserts characters immediately. @@ -119,6 +120,7 @@ impl ChatComposer { has_focus: has_input_focus, attached_images: Vec::new(), placeholder_text, + is_task_running: false, paste_burst: PasteBurst::default(), disable_paste_burst: false, custom_prompts: Vec::new(), @@ -1205,6 +1207,10 @@ impl ChatComposer { self.has_focus = has_focus; } + pub fn set_task_running(&mut self, running: bool) { + self.is_task_running = running; + } + pub(crate) fn set_esc_backtrack_hint(&mut self, show: bool) { self.esc_backtrack_hint = show; } @@ -1229,11 +1235,16 @@ impl WidgetRef for ChatComposer { ActivePopup::None => { let bottom_line_rect = popup_rect; let mut hint: Vec> = if self.ctrl_c_quit_hint { + let ctrl_c_followup = if self.is_task_running { + " to interrupt" + } else { + " to quit" + }; vec![ " ".into(), key_hint::ctrl('C'), " again".into(), - " to quit".into(), + ctrl_c_followup.into(), ] } else { let newline_hint_key = if self.use_shift_enter_hint { diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 88b3f09646..8d84ccc121 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -30,8 +30,8 @@ mod textarea; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum CancellationEvent { - Ignored, Handled, + NotHandled, } pub(crate) use chat_composer::ChatComposer; @@ -195,7 +195,15 @@ impl BottomPane { pub(crate) fn on_ctrl_c(&mut self) -> CancellationEvent { let mut view = match self.active_view.take() { Some(view) => view, - None => return CancellationEvent::Ignored, + None => { + return if self.composer_is_empty() { + CancellationEvent::NotHandled + } else { + self.set_composer_text(String::new()); + self.show_ctrl_c_quit_hint(); + CancellationEvent::Handled + }; + } }; let event = view.on_ctrl_c(self); @@ -208,7 +216,7 @@ impl BottomPane { } self.show_ctrl_c_quit_hint(); } - CancellationEvent::Ignored => { + CancellationEvent::NotHandled => { self.active_view = Some(view); } } @@ -267,6 +275,7 @@ impl BottomPane { } } + #[cfg(test)] pub(crate) fn ctrl_c_quit_hint_visible(&self) -> bool { self.ctrl_c_quit_hint } @@ -289,6 +298,7 @@ impl BottomPane { pub fn set_task_running(&mut self, running: bool) { self.is_task_running = running; + self.composer.set_task_running(running); if running { if self.status.is_none() { @@ -504,7 +514,7 @@ mod tests { let tx = AppEventSender::new(tx_raw); let mut pane = BottomPane::new(BottomPaneParams { app_event_tx: tx, - frame_requester: crate::tui::FrameRequester::test_dummy(), + frame_requester: FrameRequester::test_dummy(), has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), @@ -513,7 +523,7 @@ mod tests { pane.push_approval_request(exec_request()); assert_eq!(CancellationEvent::Handled, pane.on_ctrl_c()); assert!(pane.ctrl_c_quit_hint_visible()); - assert_eq!(CancellationEvent::Ignored, pane.on_ctrl_c()); + assert_eq!(CancellationEvent::NotHandled, pane.on_ctrl_c()); } // live ring removed; related tests deleted. @@ -524,7 +534,7 @@ mod tests { let tx = AppEventSender::new(tx_raw); let mut pane = BottomPane::new(BottomPaneParams { app_event_tx: tx, - frame_requester: crate::tui::FrameRequester::test_dummy(), + frame_requester: FrameRequester::test_dummy(), has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), @@ -555,7 +565,7 @@ mod tests { let tx = AppEventSender::new(tx_raw); let mut pane = BottomPane::new(BottomPaneParams { app_event_tx: tx.clone(), - frame_requester: crate::tui::FrameRequester::test_dummy(), + frame_requester: FrameRequester::test_dummy(), has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), @@ -583,7 +593,7 @@ mod tests { // Render and ensure the top row includes the Working header and a composer line below. // Give the animation thread a moment to tick. - std::thread::sleep(std::time::Duration::from_millis(120)); + std::thread::sleep(Duration::from_millis(120)); let area = Rect::new(0, 0, 40, 6); let mut buf = Buffer::empty(area); (&pane).render_ref(area, &mut buf); @@ -623,7 +633,7 @@ mod tests { let tx = AppEventSender::new(tx_raw); let mut pane = BottomPane::new(BottomPaneParams { app_event_tx: tx, - frame_requester: crate::tui::FrameRequester::test_dummy(), + frame_requester: FrameRequester::test_dummy(), has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), @@ -654,7 +664,7 @@ mod tests { let tx = AppEventSender::new(tx_raw); let mut pane = BottomPane::new(BottomPaneParams { app_event_tx: tx, - frame_requester: crate::tui::FrameRequester::test_dummy(), + frame_requester: FrameRequester::test_dummy(), has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), @@ -705,7 +715,7 @@ mod tests { let tx = AppEventSender::new(tx_raw); let mut pane = BottomPane::new(BottomPaneParams { app_event_tx: tx, - frame_requester: crate::tui::FrameRequester::test_dummy(), + frame_requester: FrameRequester::test_dummy(), has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index bee9d5e175..678eb1833a 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1288,15 +1288,17 @@ impl ChatWidget { /// Handle Ctrl-C key press. fn on_ctrl_c(&mut self) { - if self.bottom_pane.on_ctrl_c() == CancellationEvent::Ignored { - if self.bottom_pane.is_task_running() { - self.submit_op(Op::Interrupt); - } else if self.bottom_pane.ctrl_c_quit_hint_visible() { - self.submit_op(Op::Shutdown); - } else { - self.bottom_pane.show_ctrl_c_quit_hint(); - } + if self.bottom_pane.on_ctrl_c() == CancellationEvent::Handled { + return; } + + if self.bottom_pane.is_task_running() { + self.bottom_pane.show_ctrl_c_quit_hint(); + self.submit_op(Op::Interrupt); + return; + } + + self.submit_op(Op::Shutdown); } pub(crate) fn composer_is_empty(&self) -> bool { From c8fab5137290b158e57829ef096dfda28af894cd Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Sun, 7 Sep 2025 20:22:25 -0700 Subject: [PATCH 04/11] Use ConversationId instead of raw Uuids (#3282) We're trying to migrate from `session_id: Uuid` to `conversation_id: ConversationId`. Not only does this give us more type safety but it unifies our terminology across Codex and with the implementation of session resuming, a conversation (which can span multiple sessions) is more appropriate. I started this impl on https://github.com/openai/codex/pull/3219 as part of getting resume working in the extension but it's big enough that it should be broken out. --- codex-rs/core/src/client.rs | 14 +++-- codex-rs/core/src/codex.rs | 34 ++++++----- codex-rs/core/src/conversation_manager.rs | 20 +++---- codex-rs/core/src/error.rs | 4 +- codex-rs/core/src/message_history.rs | 15 +++-- codex-rs/core/src/rollout/recorder.rs | 23 ++++---- .../core/tests/chat_completions_payload.rs | 4 +- codex-rs/core/tests/chat_completions_sse.rs | 4 +- codex-rs/core/tests/suite/client.rs | 10 ++-- .../src/event_processor_with_human_output.rs | 4 +- .../mcp-server/src/codex_message_processor.rs | 22 +++---- codex-rs/mcp-server/src/codex_tool_config.rs | 19 ++++--- codex-rs/mcp-server/src/codex_tool_runner.rs | 12 ++-- codex-rs/mcp-server/src/message_processor.rs | 48 ++++++++++------ codex-rs/mcp-server/src/outgoing_message.rs | 7 ++- .../mcp-server/tests/suite/list_resume.rs | 2 +- codex-rs/protocol/src/mcp_protocol.rs | 20 ++++++- codex-rs/protocol/src/message_history.rs | 2 +- codex-rs/protocol/src/protocol.rs | 25 ++++---- codex-rs/tui/src/app_backtrack.rs | 11 ++-- codex-rs/tui/src/chatwidget.rs | 16 +++--- codex-rs/tui/src/chatwidget/tests.rs | 57 ++++++++++--------- codex-rs/tui/src/history_cell.rs | 4 +- 23 files changed, 213 insertions(+), 164 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 52c034ebba..2050d8e219 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -6,6 +6,7 @@ use std::time::Duration; use crate::AuthManager; use bytes::Bytes; use codex_protocol::mcp_protocol::AuthMode; +use codex_protocol::mcp_protocol::ConversationId; use eventsource_stream::Eventsource; use futures::prelude::*; use regex_lite::Regex; @@ -19,7 +20,6 @@ use tokio_util::io::ReaderStream; use tracing::debug; use tracing::trace; use tracing::warn; -use uuid::Uuid; use crate::chat_completions::AggregateStreamExt; use crate::chat_completions::stream_chat_completions; @@ -70,7 +70,7 @@ pub struct ModelClient { auth_manager: Option>, client: reqwest::Client, provider: ModelProviderInfo, - session_id: Uuid, + conversation_id: ConversationId, effort: ReasoningEffortConfig, summary: ReasoningSummaryConfig, } @@ -82,7 +82,7 @@ impl ModelClient { provider: ModelProviderInfo, effort: ReasoningEffortConfig, summary: ReasoningSummaryConfig, - session_id: Uuid, + conversation_id: ConversationId, ) -> Self { let client = create_client(&config.responses_originator_header); @@ -91,7 +91,7 @@ impl ModelClient { auth_manager, client, provider, - session_id, + conversation_id, effort, summary, } @@ -197,7 +197,7 @@ impl ModelClient { store: false, stream: true, include, - prompt_cache_key: Some(self.session_id.to_string()), + prompt_cache_key: Some(self.conversation_id.to_string()), text, }; @@ -223,7 +223,9 @@ impl ModelClient { req_builder = req_builder .header("OpenAI-Beta", "responses=experimental") - .header("session_id", self.session_id.to_string()) + // Send session_id for compatibility. + .header("conversation_id", self.conversation_id.to_string()) + .header("session_id", self.conversation_id.to_string()) .header(reqwest::header::ACCEPT, "text/event-stream") .json(&payload); diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 95e9a96951..639493b427 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -15,6 +15,7 @@ use async_channel::Sender; use codex_apply_patch::ApplyPatchAction; use codex_apply_patch::MaybeApplyPatchVerified; use codex_apply_patch::maybe_parse_apply_patch_verified; +use codex_protocol::mcp_protocol::ConversationId; use codex_protocol::protocol::ConversationHistoryResponseEvent; use codex_protocol::protocol::TaskStartedEvent; use codex_protocol::protocol::TurnAbortReason; @@ -149,7 +150,7 @@ pub struct Codex { /// unique session id. pub struct CodexSpawnOk { pub codex: Codex, - pub session_id: Uuid, + pub conversation_id: ConversationId, } pub(crate) const INITIAL_SUBMIT_ID: &str = ""; @@ -205,7 +206,7 @@ impl Codex { session .record_initial_history(&turn_context, conversation_history) .await; - let session_id = session.session_id; + let conversation_id = session.conversation_id; // This task will run until Op::Shutdown is received. tokio::spawn(submission_loop( @@ -220,7 +221,10 @@ impl Codex { rx_event, }; - Ok(CodexSpawnOk { codex, session_id }) + Ok(CodexSpawnOk { + codex, + conversation_id, + }) } /// Submit the `op` wrapped in a `Submission` with a unique ID. @@ -269,7 +273,7 @@ struct State { /// /// A session has at most 1 running task at a time, and can be interrupted by user input. pub(crate) struct Session { - session_id: Uuid, + conversation_id: ConversationId, tx_event: Sender, /// Manager for external MCP servers/tools. @@ -358,7 +362,7 @@ impl Session { tx_event: Sender, initial_history: InitialHistory, ) -> anyhow::Result<(Arc, TurnContext)> { - let session_id = Uuid::new_v4(); + let conversation_id = ConversationId::from(Uuid::new_v4()); let ConfigureSession { provider, model, @@ -385,7 +389,7 @@ impl Session { // - spin up MCP connection manager // - perform default shell discovery // - load history metadata - let rollout_fut = RolloutRecorder::new(&config, session_id, user_instructions.clone()); + let rollout_fut = RolloutRecorder::new(&config, conversation_id, user_instructions.clone()); let mcp_fut = McpConnectionManager::new(config.mcp_servers.clone()); let default_shell_fut = shell::default_user_shell(); @@ -431,7 +435,7 @@ impl Session { } } - // Now that `session_id` is final (may have been updated by resume), + // Now that the conversation id is final (may have been updated by resume), // construct the model client. let client = ModelClient::new( config.clone(), @@ -439,7 +443,7 @@ impl Session { provider.clone(), model_reasoning_effort, model_reasoning_summary, - session_id, + conversation_id, ); let turn_context = TurnContext { client, @@ -461,7 +465,7 @@ impl Session { cwd, }; let sess = Arc::new(Session { - session_id, + conversation_id, tx_event: tx_event.clone(), mcp_connection_manager, session_manager: ExecSessionManager::default(), @@ -483,7 +487,7 @@ impl Session { let events = std::iter::once(Event { id: INITIAL_SUBMIT_ID.to_owned(), msg: EventMsg::SessionConfigured(SessionConfiguredEvent { - session_id, + session_id: conversation_id, model, history_log_id, history_entry_count, @@ -1084,7 +1088,7 @@ async fn submission_loop( provider, effective_effort, effective_summary, - sess.session_id, + sess.conversation_id, ); let new_approval_policy = approval_policy.unwrap_or(prev.approval_policy); @@ -1172,7 +1176,7 @@ async fn submission_loop( provider, effort, summary, - sess.session_id, + sess.conversation_id, ); let fresh_turn_context = TurnContext { @@ -1215,7 +1219,7 @@ async fn submission_loop( other => sess.notify_approval(&id, other), }, Op::AddToHistory { text } => { - let id = sess.session_id; + let id = sess.conversation_id; let config = config.clone(); tokio::spawn(async move { if let Err(e) = crate::message_history::append_entry(&text, &id, &config).await @@ -1246,7 +1250,7 @@ async fn submission_loop( log_id, entry: entry_opt.map(|e| { codex_protocol::message_history::HistoryEntry { - session_id: e.session_id, + conversation_id: e.session_id, ts: e.ts, text: e.text, } @@ -1352,7 +1356,7 @@ async fn submission_loop( let event = Event { id: sub_id.clone(), msg: EventMsg::ConversationHistory(ConversationHistoryResponseEvent { - conversation_id: sess.session_id, + conversation_id: sess.conversation_id, entries: sess.state.lock_unchecked().history.contents(), }), }; diff --git a/codex-rs/core/src/conversation_manager.rs b/codex-rs/core/src/conversation_manager.rs index 78674cd7e7..6cab2760a3 100644 --- a/codex-rs/core/src/conversation_manager.rs +++ b/codex-rs/core/src/conversation_manager.rs @@ -4,8 +4,8 @@ use std::sync::Arc; use crate::AuthManager; use crate::CodexAuth; +use codex_protocol::mcp_protocol::ConversationId; use tokio::sync::RwLock; -use uuid::Uuid; use crate::codex::Codex; use crate::codex::CodexSpawnOk; @@ -29,7 +29,7 @@ pub enum InitialHistory { /// Represents a newly created Codex conversation, including the first event /// (which is [`EventMsg::SessionConfigured`]). pub struct NewConversation { - pub conversation_id: Uuid, + pub conversation_id: ConversationId, pub conversation: Arc, pub session_configured: SessionConfiguredEvent, } @@ -37,7 +37,7 @@ pub struct NewConversation { /// [`ConversationManager`] is responsible for creating conversations and /// maintaining them in memory. pub struct ConversationManager { - conversations: Arc>>>, + conversations: Arc>>>, auth_manager: Arc, } @@ -70,13 +70,13 @@ impl ConversationManager { let initial_history = RolloutRecorder::get_rollout_history(resume_path).await?; let CodexSpawnOk { codex, - session_id: conversation_id, + conversation_id, } = Codex::spawn(config, auth_manager, initial_history).await?; self.finalize_spawn(codex, conversation_id).await } else { let CodexSpawnOk { codex, - session_id: conversation_id, + conversation_id, } = { Codex::spawn(config, auth_manager, InitialHistory::New).await? }; self.finalize_spawn(codex, conversation_id).await } @@ -85,7 +85,7 @@ impl ConversationManager { async fn finalize_spawn( &self, codex: Codex, - conversation_id: Uuid, + conversation_id: ConversationId, ) -> CodexResult { // The first event must be `SessionInitialized`. Validate and forward it // to the caller so that they can display it in the conversation @@ -116,7 +116,7 @@ impl ConversationManager { pub async fn get_conversation( &self, - conversation_id: Uuid, + conversation_id: ConversationId, ) -> CodexResult> { let conversations = self.conversations.read().await; conversations @@ -134,12 +134,12 @@ impl ConversationManager { let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?; let CodexSpawnOk { codex, - session_id: conversation_id, + conversation_id, } = Codex::spawn(config, auth_manager, initial_history).await?; self.finalize_spawn(codex, conversation_id).await } - pub async fn remove_conversation(&self, conversation_id: Uuid) { + pub async fn remove_conversation(&self, conversation_id: ConversationId) { self.conversations.write().await.remove(&conversation_id); } @@ -161,7 +161,7 @@ impl ConversationManager { let auth_manager = self.auth_manager.clone(); let CodexSpawnOk { codex, - session_id: conversation_id, + conversation_id, } = Codex::spawn(config, auth_manager, history).await?; self.finalize_spawn(codex, conversation_id).await diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index 00ac145c2e..36b7abe677 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -1,10 +1,10 @@ +use codex_protocol::mcp_protocol::ConversationId; use reqwest::StatusCode; use serde_json; use std::io; use std::time::Duration; use thiserror::Error; use tokio::task::JoinError; -use uuid::Uuid; pub type Result = std::result::Result; @@ -49,7 +49,7 @@ pub enum CodexErr { Stream(String, Option), #[error("no conversation with id: {0}")] - ConversationNotFound(Uuid), + ConversationNotFound(ConversationId), #[error("session configured event was not the first event in the stream")] SessionConfiguredNotFirstEvent, diff --git a/codex-rs/core/src/message_history.rs b/codex-rs/core/src/message_history.rs index 75bc5aa75c..bfdbde6781 100644 --- a/codex-rs/core/src/message_history.rs +++ b/codex-rs/core/src/message_history.rs @@ -5,7 +5,7 @@ //! JSON-Lines tooling. Each record has the following schema: //! //! ````text -//! {"session_id":"","ts":,"text":""} +//! {"conversation_id":"","ts":,"text":""} //! ```` //! //! To minimise the chance of interleaved writes when multiple processes are @@ -22,10 +22,11 @@ use std::path::PathBuf; use serde::Deserialize; use serde::Serialize; + +use codex_protocol::mcp_protocol::ConversationId; use std::time::Duration; use tokio::fs; use tokio::io::AsyncReadExt; -use uuid::Uuid; use crate::config::Config; use crate::config_types::HistoryPersistence; @@ -54,10 +55,14 @@ fn history_filepath(config: &Config) -> PathBuf { path } -/// Append a `text` entry associated with `session_id` to the history file. Uses +/// Append a `text` entry associated with `conversation_id` to the history file. Uses /// advisory file locking to ensure that concurrent writes do not interleave, /// which entails a small amount of blocking I/O internally. -pub(crate) async fn append_entry(text: &str, session_id: &Uuid, config: &Config) -> Result<()> { +pub(crate) async fn append_entry( + text: &str, + conversation_id: &ConversationId, + config: &Config, +) -> Result<()> { match config.history.persistence { HistoryPersistence::SaveAll => { // Save everything: proceed. @@ -84,7 +89,7 @@ pub(crate) async fn append_entry(text: &str, session_id: &Uuid, config: &Config) // Construct the JSON line first so we can write it in a single syscall. let entry = HistoryEntry { - session_id: session_id.to_string(), + session_id: conversation_id.to_string(), ts, text: text.to_string(), }; diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index 10fb64be15..66a65a95e9 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -5,6 +5,7 @@ use std::fs::{self}; use std::io::Error as IoError; use std::path::Path; +use codex_protocol::mcp_protocol::ConversationId; use serde::Deserialize; use serde::Serialize; use serde_json::Value; @@ -17,7 +18,6 @@ use tokio::sync::mpsc::{self}; use tokio::sync::oneshot; use tracing::info; use tracing::warn; -use uuid::Uuid; use super::SESSIONS_SUBDIR; use super::list::ConversationsPage; @@ -32,7 +32,7 @@ use codex_protocol::models::ResponseItem; #[derive(Serialize, Deserialize, Clone, Default)] pub struct SessionMeta { - pub id: Uuid, + pub id: ConversationId, pub timestamp: String, pub instructions: Option, } @@ -55,7 +55,7 @@ pub struct SavedSession { pub items: Vec, #[serde(default)] pub state: SessionStateSnapshot, - pub session_id: Uuid, + pub session_id: ConversationId, } /// Records all [`ResponseItem`]s for a session and flushes them to disk after @@ -94,14 +94,14 @@ impl RolloutRecorder { /// error so the caller can decide whether to disable persistence. pub async fn new( config: &Config, - uuid: Uuid, + conversation_id: ConversationId, instructions: Option, ) -> std::io::Result { let LogFileInfo { file, - session_id, + conversation_id: session_id, timestamp, - } = create_log_file(config, uuid)?; + } = create_log_file(config, conversation_id)?; let timestamp_format: &[FormatItem] = format_description!( "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z" @@ -227,13 +227,16 @@ struct LogFileInfo { file: File, /// Session ID (also embedded in filename). - session_id: Uuid, + conversation_id: ConversationId, /// Timestamp for the start of the session. timestamp: OffsetDateTime, } -fn create_log_file(config: &Config, session_id: Uuid) -> std::io::Result { +fn create_log_file( + config: &Config, + conversation_id: ConversationId, +) -> std::io::Result { // Resolve ~/.codex/sessions/YYYY/MM/DD and create it if missing. let timestamp = OffsetDateTime::now_local() .map_err(|e| IoError::other(format!("failed to get local time: {e}")))?; @@ -252,7 +255,7 @@ fn create_log_file(config: &Config, session_id: Uuid) -> std::io::Result std::io::Result) -> Value { provider, effort, summary, - Uuid::new_v4(), + ConversationId::new(), ); let mut prompt = Prompt::default(); diff --git a/codex-rs/core/tests/chat_completions_sse.rs b/codex-rs/core/tests/chat_completions_sse.rs index 1df658dad6..6155d15e62 100644 --- a/codex-rs/core/tests/chat_completions_sse.rs +++ b/codex-rs/core/tests/chat_completions_sse.rs @@ -8,10 +8,10 @@ use codex_core::ResponseEvent; use codex_core::ResponseItem; use codex_core::WireApi; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; +use codex_protocol::mcp_protocol::ConversationId; use core_test_support::load_default_config_for_test; use futures::StreamExt; use tempfile::TempDir; -use uuid::Uuid; use wiremock::Mock; use wiremock::MockServer; use wiremock::ResponseTemplate; @@ -69,7 +69,7 @@ async fn run_stream(sse_body: &str) -> Vec { provider, effort, summary, - Uuid::new_v4(), + ConversationId::new(), ); let mut prompt = Prompt::default(); diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index cc1369e76f..70e6e2b670 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -242,7 +242,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn includes_session_id_and_model_headers_in_request() { +async fn includes_conversation_id_and_model_headers_in_request() { if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { println!( "Skipping test because it cannot execute when network is disabled in a Codex sandbox." @@ -299,12 +299,12 @@ async fn includes_session_id_and_model_headers_in_request() { // get request from the server let request = &server.received_requests().await.unwrap()[0]; - let request_session_id = request.headers.get("session_id").unwrap(); + let request_conversation_id = request.headers.get("conversation_id").unwrap(); let request_authorization = request.headers.get("authorization").unwrap(); let request_originator = request.headers.get("originator").unwrap(); assert_eq!( - request_session_id.to_str().unwrap(), + request_conversation_id.to_str().unwrap(), conversation_id.to_string() ); assert_eq!(request_originator.to_str().unwrap(), "codex_cli_rs"); @@ -477,14 +477,14 @@ async fn chatgpt_auth_sends_correct_request() { // get request from the server let request = &server.received_requests().await.unwrap()[0]; - let request_session_id = request.headers.get("session_id").unwrap(); + let request_conversation_id = request.headers.get("conversation_id").unwrap(); let request_authorization = request.headers.get("authorization").unwrap(); let request_originator = request.headers.get("originator").unwrap(); let request_chatgpt_account_id = request.headers.get("chatgpt-account-id").unwrap(); let request_body = request.body_json::().unwrap(); assert_eq!( - request_session_id.to_str().unwrap(), + request_conversation_id.to_str().unwrap(), conversation_id.to_string() ); assert_eq!(request_originator.to_str().unwrap(), "codex_cli_rs"); diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 3c639ad25a..71e718c5a7 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -517,7 +517,7 @@ impl EventProcessor for EventProcessorWithHumanOutput { } EventMsg::SessionConfigured(session_configured_event) => { let SessionConfiguredEvent { - session_id, + session_id: conversation_id, model, history_log_id: _, history_entry_count: _, @@ -528,7 +528,7 @@ impl EventProcessor for EventProcessorWithHumanOutput { self, "{} {}", "codex session".style(self.magenta).style(self.bold), - session_id.to_string().style(self.dimmed) + conversation_id.to_string().style(self.dimmed) ); ts_println!(self, "model: {}", model); diff --git a/codex-rs/mcp-server/src/codex_message_processor.rs b/codex-rs/mcp-server/src/codex_message_processor.rs index 828cf1f167..d7502b2376 100644 --- a/codex-rs/mcp-server/src/codex_message_processor.rs +++ b/codex-rs/mcp-server/src/codex_message_processor.rs @@ -99,7 +99,7 @@ pub(crate) struct CodexMessageProcessor { conversation_listeners: HashMap>, active_login: Arc>>, // Queue of pending interrupt requests per conversation. We reply when TurnAborted arrives. - pending_interrupts: Arc>>>, + pending_interrupts: Arc>>>, } impl CodexMessageProcessor { @@ -511,7 +511,7 @@ impl CodexMessageProcessor { .. } = conversation_id; let response = NewConversationResponse { - conversation_id: ConversationId(conversation_id), + conversation_id, model: session_configured.model, }; self.outgoing.send_response(request_id, response).await; @@ -632,7 +632,7 @@ impl CodexMessageProcessor { // Reply with conversation id + model and initial messages (when present) let response = codex_protocol::mcp_protocol::ResumeConversationResponse { - conversation_id: ConversationId(conversation_id), + conversation_id, model: session_configured.model.clone(), initial_messages: session_configured.initial_messages.clone(), }; @@ -656,7 +656,7 @@ impl CodexMessageProcessor { } = params; let Ok(conversation) = self .conversation_manager - .get_conversation(conversation_id.0) + .get_conversation(conversation_id) .await else { let error = JSONRPCErrorError { @@ -704,7 +704,7 @@ impl CodexMessageProcessor { let Ok(conversation) = self .conversation_manager - .get_conversation(conversation_id.0) + .get_conversation(conversation_id) .await else { let error = JSONRPCErrorError { @@ -750,7 +750,7 @@ impl CodexMessageProcessor { let InterruptConversationParams { conversation_id } = params; let Ok(conversation) = self .conversation_manager - .get_conversation(conversation_id.0) + .get_conversation(conversation_id) .await else { let error = JSONRPCErrorError { @@ -765,7 +765,7 @@ impl CodexMessageProcessor { // Record the pending interrupt so we can reply when TurnAborted arrives. { let mut map = self.pending_interrupts.lock().await; - map.entry(conversation_id.0).or_default().push(request_id); + map.entry(conversation_id).or_default().push(request_id); } // Submit the interrupt; we'll respond upon TurnAborted. @@ -780,12 +780,12 @@ impl CodexMessageProcessor { let AddConversationListenerParams { conversation_id } = params; let Ok(conversation) = self .conversation_manager - .get_conversation(conversation_id.0) + .get_conversation(conversation_id) .await else { let error = JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, - message: format!("conversation not found: {}", conversation_id.0), + message: format!("conversation not found: {conversation_id}"), data: None, }; self.outgoing.send_error(request_id, error).await; @@ -898,7 +898,7 @@ async fn apply_bespoke_event_handling( conversation_id: ConversationId, conversation: Arc, outgoing: Arc, - pending_interrupts: Arc>>>, + pending_interrupts: Arc>>>, ) { let Event { id: event_id, msg } = event; match msg { @@ -951,7 +951,7 @@ async fn apply_bespoke_event_handling( EventMsg::TurnAborted(turn_aborted_event) => { let pending = { let mut map = pending_interrupts.lock().await; - map.remove(&conversation_id.0).unwrap_or_default() + map.remove(&conversation_id).unwrap_or_default() }; if !pending.is_empty() { let response = InterruptConversationResponse { diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index 0ba5593fcb..7b635d2dc3 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -181,8 +181,8 @@ impl CodexToolCallParam { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct CodexToolCallReplyParam { - /// The *session id* for this conversation. - pub session_id: String, + /// The conversation id for this Codex session. + pub conversation_id: String, /// The *next user prompt* to continue the Codex conversation. pub prompt: String, @@ -213,7 +213,8 @@ pub(crate) fn create_tool_for_codex_tool_call_reply_param() -> Tool { input_schema: tool_input_schema, output_schema: None, description: Some( - "Continue a Codex session by providing the session id and prompt.".to_string(), + "Continue a Codex conversation by providing the conversation id and prompt." + .to_string(), ), annotations: None, } @@ -308,21 +309,21 @@ mod tests { let tool = create_tool_for_codex_tool_call_reply_param(); let tool_json = serde_json::to_value(&tool).expect("tool serializes"); let expected_tool_json = serde_json::json!({ - "description": "Continue a Codex session by providing the session id and prompt.", + "description": "Continue a Codex conversation by providing the conversation id and prompt.", "inputSchema": { "properties": { + "conversationId": { + "description": "The conversation id for this Codex session.", + "type": "string" + }, "prompt": { "description": "The *next user prompt* to continue the Codex conversation.", "type": "string" }, - "sessionId": { - "description": "The *session id* for this conversation.", - "type": "string" - }, }, "required": [ + "conversationId", "prompt", - "sessionId", ], "type": "object", }, diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 48d520b5d6..4be144856b 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -18,13 +18,13 @@ use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::Submission; use codex_core::protocol::TaskCompleteEvent; +use codex_protocol::mcp_protocol::ConversationId; use mcp_types::CallToolResult; use mcp_types::ContentBlock; use mcp_types::RequestId; use mcp_types::TextContent; use serde_json::json; use tokio::sync::Mutex; -use uuid::Uuid; use crate::exec_approval::handle_exec_approval_request; use crate::outgoing_message::OutgoingMessageSender; @@ -43,7 +43,7 @@ pub async fn run_codex_tool_session( config: CodexConfig, outgoing: Arc, conversation_manager: Arc, - running_requests_id_to_codex_uuid: Arc>>, + running_requests_id_to_codex_uuid: Arc>>, ) { let NewConversation { conversation_id, @@ -119,13 +119,13 @@ pub async fn run_codex_tool_session_reply( outgoing: Arc, request_id: RequestId, prompt: String, - running_requests_id_to_codex_uuid: Arc>>, - session_id: Uuid, + running_requests_id_to_codex_uuid: Arc>>, + conversation_id: ConversationId, ) { running_requests_id_to_codex_uuid .lock() .await - .insert(request_id.clone(), session_id); + .insert(request_id.clone(), conversation_id); if let Err(e) = conversation .submit(Op::UserInput { items: vec![InputItem::Text { text: prompt }], @@ -154,7 +154,7 @@ async fn run_codex_tool_session_inner( codex: Arc, outgoing: Arc, request_id: RequestId, - running_requests_id_to_codex_uuid: Arc>>, + running_requests_id_to_codex_uuid: Arc>>, ) { let request_id_str = match &request_id { RequestId::String(s) => s.clone(), diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index bf5dbf9804..8179cdd679 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -9,6 +9,7 @@ use crate::codex_tool_config::create_tool_for_codex_tool_call_reply_param; use crate::error_code::INVALID_REQUEST_ERROR_CODE; use crate::outgoing_message::OutgoingMessageSender; use codex_protocol::mcp_protocol::ClientRequest; +use codex_protocol::mcp_protocol::ConversationId; use codex_core::AuthManager; use codex_core::ConversationManager; @@ -41,7 +42,7 @@ pub(crate) struct MessageProcessor { initialized: bool, codex_linux_sandbox_exe: Option, conversation_manager: Arc, - running_requests_id_to_codex_uuid: Arc>>, + running_requests_id_to_codex_uuid: Arc>>, } impl MessageProcessor { @@ -436,7 +437,10 @@ impl MessageProcessor { tracing::info!("tools/call -> params: {:?}", arguments); // parse arguments - let CodexToolCallReplyParam { session_id, prompt } = match arguments { + let CodexToolCallReplyParam { + conversation_id, + prompt, + } = match arguments { Some(json_val) => match serde_json::from_value::(json_val) { Ok(params) => params, Err(e) => { @@ -457,12 +461,12 @@ impl MessageProcessor { }, None => { tracing::error!( - "Missing arguments for codex-reply tool-call; the `session_id` and `prompt` fields are required." + "Missing arguments for codex-reply tool-call; the `conversation_id` and `prompt` fields are required." ); let result = CallToolResult { content: vec![ContentBlock::TextContent(TextContent { r#type: "text".to_owned(), - text: "Missing arguments for codex-reply tool-call; the `session_id` and `prompt` fields are required.".to_owned(), + text: "Missing arguments for codex-reply tool-call; the `conversation_id` and `prompt` fields are required.".to_owned(), annotations: None, })], is_error: Some(true), @@ -473,14 +477,14 @@ impl MessageProcessor { return; } }; - let session_id = match Uuid::parse_str(&session_id) { - Ok(id) => id, + let conversation_id = match Uuid::parse_str(&conversation_id) { + Ok(id) => ConversationId::from(id), Err(e) => { - tracing::error!("Failed to parse session_id: {e}"); + tracing::error!("Failed to parse conversation_id: {e}"); let result = CallToolResult { content: vec![ContentBlock::TextContent(TextContent { r#type: "text".to_owned(), - text: format!("Failed to parse session_id: {e}"), + text: format!("Failed to parse conversation_id: {e}"), annotations: None, })], is_error: Some(true), @@ -496,14 +500,18 @@ impl MessageProcessor { let outgoing = self.outgoing.clone(); let running_requests_id_to_codex_uuid = self.running_requests_id_to_codex_uuid.clone(); - let codex = match self.conversation_manager.get_conversation(session_id).await { + let codex = match self + .conversation_manager + .get_conversation(conversation_id) + .await + { Ok(c) => c, Err(_) => { - tracing::warn!("Session not found for session_id: {session_id}"); + tracing::warn!("Session not found for conversation_id: {conversation_id}"); let result = CallToolResult { content: vec![ContentBlock::TextContent(TextContent { r#type: "text".to_owned(), - text: format!("Session not found for session_id: {session_id}"), + text: format!("Session not found for conversation_id: {conversation_id}"), annotations: None, })], is_error: Some(true), @@ -528,7 +536,7 @@ impl MessageProcessor { request_id, prompt, running_requests_id_to_codex_uuid, - session_id, + conversation_id, ) .await; } @@ -564,24 +572,28 @@ impl MessageProcessor { RequestId::Integer(i) => i.to_string(), }; - // Obtain the session_id while holding the first lock, then release. - let session_id = { + // Obtain the conversation id while holding the first lock, then release. + let conversation_id = { let map_guard = self.running_requests_id_to_codex_uuid.lock().await; match map_guard.get(&request_id) { - Some(id) => *id, // Uuid is Copy + Some(id) => *id, None => { tracing::warn!("Session not found for request_id: {}", request_id_string); return; } } }; - tracing::info!("session_id: {session_id}"); + tracing::info!("conversation_id: {conversation_id}"); // Obtain the Codex conversation from the server. - let codex_arc = match self.conversation_manager.get_conversation(session_id).await { + let codex_arc = match self + .conversation_manager + .get_conversation(conversation_id) + .await + { Ok(c) => c, Err(_) => { - tracing::warn!("Session not found for session_id: {session_id}"); + tracing::warn!("Session not found for conversation_id: {conversation_id}"); return; } }; diff --git a/codex-rs/mcp-server/src/outgoing_message.rs b/codex-rs/mcp-server/src/outgoing_message.rs index b735f2bf9c..537d29db62 100644 --- a/codex-rs/mcp-server/src/outgoing_message.rs +++ b/codex-rs/mcp-server/src/outgoing_message.rs @@ -258,6 +258,7 @@ pub(crate) struct OutgoingError { mod tests { use codex_core::protocol::EventMsg; use codex_core::protocol::SessionConfiguredEvent; + use codex_protocol::mcp_protocol::ConversationId; use codex_protocol::mcp_protocol::LoginChatGptCompleteNotification; use pretty_assertions::assert_eq; use serde_json::json; @@ -270,10 +271,11 @@ mod tests { let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded_channel::(); let outgoing_message_sender = OutgoingMessageSender::new(outgoing_tx); + let conversation_id = ConversationId::new(); let event = Event { id: "1".to_string(), msg: EventMsg::SessionConfigured(SessionConfiguredEvent { - session_id: Uuid::new_v4(), + session_id: conversation_id, model: "gpt-4o".to_string(), history_log_id: 1, history_entry_count: 1000, @@ -302,8 +304,9 @@ mod tests { let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded_channel::(); let outgoing_message_sender = OutgoingMessageSender::new(outgoing_tx); + let conversation_id = ConversationId::new(); let session_configured_event = SessionConfiguredEvent { - session_id: Uuid::new_v4(), + session_id: conversation_id, model: "gpt-4o".to_string(), history_log_id: 1, history_entry_count: 1000, diff --git a/codex-rs/mcp-server/tests/suite/list_resume.rs b/codex-rs/mcp-server/tests/suite/list_resume.rs index d835e5ca77..987349228f 100644 --- a/codex-rs/mcp-server/tests/suite/list_resume.rs +++ b/codex-rs/mcp-server/tests/suite/list_resume.rs @@ -142,7 +142,7 @@ async fn test_list_and_resume_conversations() { } = to_response::(resume_resp) .expect("deserialize resumeConversation response"); // conversation id should be a valid UUID - let _ = uuid::Uuid::from_bytes(conversation_id.0.into_bytes()); + let _: uuid::Uuid = conversation_id.into(); } fn create_fake_rollout(codex_home: &Path, filename_ts: &str, meta_rfc3339: &str, preview: &str) { diff --git a/codex-rs/protocol/src/mcp_protocol.rs b/codex-rs/protocol/src/mcp_protocol.rs index 26d39eb6a8..a8d4b1807f 100644 --- a/codex-rs/protocol/src/mcp_protocol.rs +++ b/codex-rs/protocol/src/mcp_protocol.rs @@ -19,16 +19,34 @@ use strum_macros::Display; use ts_rs::TS; use uuid::Uuid; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS, Hash, Default)] #[ts(type = "string")] pub struct ConversationId(pub Uuid); +impl ConversationId { + pub fn new() -> Self { + Self(Uuid::new_v4()) + } +} + impl Display for ConversationId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } +impl From for ConversationId { + fn from(value: Uuid) -> Self { + Self(value) + } +} + +impl From for Uuid { + fn from(value: ConversationId) -> Self { + value.0 + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, TS)] #[ts(type = "string")] pub struct GitSha(pub String); diff --git a/codex-rs/protocol/src/message_history.rs b/codex-rs/protocol/src/message_history.rs index 3a561df7bf..5d3799a540 100644 --- a/codex-rs/protocol/src/message_history.rs +++ b/codex-rs/protocol/src/message_history.rs @@ -3,7 +3,7 @@ use serde::Serialize; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct HistoryEntry { - pub session_id: String, + pub conversation_id: String, pub ts: u64, pub text: String, } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index a422327db3..6aa9d136bc 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -10,7 +10,14 @@ use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; +use crate::config_types::ReasoningEffort as ReasoningEffortConfig; +use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::custom_prompts::CustomPrompt; +use crate::mcp_protocol::ConversationId; +use crate::message_history::HistoryEntry; +use crate::models::ResponseItem; +use crate::parse_command::ParsedCommand; +use crate::plan_tool::UpdatePlanArgs; use mcp_types::CallToolResult; use mcp_types::Tool as McpTool; use serde::Deserialize; @@ -18,14 +25,6 @@ use serde::Serialize; use serde_with::serde_as; use strum_macros::Display; use ts_rs::TS; -use uuid::Uuid; - -use crate::config_types::ReasoningEffort as ReasoningEffortConfig; -use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; -use crate::message_history::HistoryEntry; -use crate::models::ResponseItem; -use crate::parse_command::ParsedCommand; -use crate::plan_tool::UpdatePlanArgs; /// Open/close tags for special user-input blocks. Used across crates to avoid /// duplicated hardcoded strings. @@ -791,7 +790,7 @@ pub struct WebSearchEndEvent { /// in-memory transcript. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ConversationHistoryResponseEvent { - pub conversation_id: Uuid, + pub conversation_id: ConversationId, pub entries: Vec, } @@ -931,8 +930,8 @@ pub struct ListCustomPromptsResponseEvent { #[derive(Debug, Default, Clone, Deserialize, Serialize)] pub struct SessionConfiguredEvent { - /// Unique id for this session. - pub session_id: Uuid, + /// Name left as session_id instead of conversation_id for backwards compatibility. + pub session_id: ConversationId, /// Tell the client what model is being queried. pub model: String, @@ -1014,11 +1013,11 @@ mod tests { /// amount of nesting. #[test] fn serialize_event() { - let session_id: Uuid = uuid::uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8"); + let conversation_id = ConversationId(uuid::uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8")); let event = Event { id: "1234".to_string(), msg: EventMsg::SessionConfigured(SessionConfiguredEvent { - session_id, + session_id: conversation_id, model: "codex-mini-latest".to_string(), history_log_id: 0, history_entry_count: 0, diff --git a/codex-rs/tui/src/app_backtrack.rs b/codex-rs/tui/src/app_backtrack.rs index c716d6c4ab..b893fd79d0 100644 --- a/codex-rs/tui/src/app_backtrack.rs +++ b/codex-rs/tui/src/app_backtrack.rs @@ -4,6 +4,7 @@ use crate::pager_overlay::Overlay; use crate::tui; use crate::tui::TuiEvent; use codex_core::protocol::ConversationHistoryResponseEvent; +use codex_protocol::mcp_protocol::ConversationId; use color_eyre::eyre::Result; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; @@ -14,13 +15,13 @@ pub(crate) struct BacktrackState { /// True when Esc has primed backtrack mode in the main view. pub(crate) primed: bool, /// Session id of the base conversation to fork from. - pub(crate) base_id: Option, + pub(crate) base_id: Option, /// Current step count (Nth last user message). pub(crate) count: usize, /// True when the transcript overlay is showing a backtrack preview. pub(crate) overlay_preview_active: bool, /// Pending fork request: (base_id, drop_count, prefill). - pub(crate) pending: Option<(uuid::Uuid, usize, String)>, + pub(crate) pending: Option<(ConversationId, usize, String)>, } impl App { @@ -91,7 +92,7 @@ impl App { pub(crate) fn request_backtrack( &mut self, prefill: String, - base_id: uuid::Uuid, + base_id: ConversationId, drop_last_messages: usize, ) { self.backtrack.pending = Some((base_id, drop_last_messages, prefill)); @@ -135,7 +136,7 @@ impl App { fn prime_backtrack(&mut self) { self.backtrack.primed = true; self.backtrack.count = 0; - self.backtrack.base_id = self.chat_widget.session_id(); + self.backtrack.base_id = self.chat_widget.conversation_id(); self.chat_widget.show_esc_backtrack_hint(); } @@ -151,7 +152,7 @@ impl App { /// When overlay is already open, begin preview mode and select latest user message. fn begin_overlay_backtrack_preview(&mut self, tui: &mut tui::Tui) { self.backtrack.primed = true; - self.backtrack.base_id = self.chat_widget.session_id(); + self.backtrack.base_id = self.chat_widget.conversation_id(); self.backtrack.overlay_preview_active = true; let sel = self.compute_backtrack_selection(tui, 1); self.apply_backtrack_selection(sel); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 678eb1833a..dca821f255 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -85,7 +85,7 @@ use codex_core::protocol::AskForApproval; use codex_core::protocol::SandboxPolicy; use codex_core::protocol_config_types::ReasoningEffort as ReasoningEffortConfig; use codex_file_search::FileMatch; -use uuid::Uuid; +use codex_protocol::mcp_protocol::ConversationId; // Track information about an in-flight exec command. struct RunningCommand { @@ -121,7 +121,7 @@ pub(crate) struct ChatWidget { reasoning_buffer: String, // Accumulates full reasoning content for transcript-only recording full_reasoning_buffer: String, - session_id: Option, + conversation_id: Option, frame_requester: FrameRequester, // Whether to include the initial welcome banner on session configured show_welcome_banner: bool, @@ -163,7 +163,7 @@ impl ChatWidget { fn on_session_configured(&mut self, event: codex_core::protocol::SessionConfiguredEvent) { self.bottom_pane .set_history_metadata(event.history_log_id, event.history_entry_count); - self.session_id = Some(event.session_id); + self.conversation_id = Some(event.session_id); let initial_messages = event.initial_messages.clone(); if let Some(messages) = initial_messages { self.replay_initial_messages(messages); @@ -660,7 +660,7 @@ impl ChatWidget { interrupts: InterruptManager::new(), reasoning_buffer: String::new(), full_reasoning_buffer: String::new(), - session_id: None, + conversation_id: None, queued_user_messages: VecDeque::new(), show_welcome_banner: true, suppress_session_configured_redraw: false, @@ -712,7 +712,7 @@ impl ChatWidget { interrupts: InterruptManager::new(), reasoning_buffer: String::new(), full_reasoning_buffer: String::new(), - session_id: None, + conversation_id: None, queued_user_messages: VecDeque::new(), show_welcome_banner: false, suppress_session_configured_redraw: true, @@ -1159,7 +1159,7 @@ impl ChatWidget { self.add_to_history(history_cell::new_status_output( &self.config, usage_ref, - &self.session_id, + &self.conversation_id, )); } @@ -1360,8 +1360,8 @@ impl ChatWidget { .unwrap_or_default() } - pub(crate) fn session_id(&self) -> Option { - self.session_id + pub(crate) fn conversation_id(&self) -> Option { + self.conversation_id } /// Return a reference to the widget's current config (includes any diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index dfcbbec7d0..e423e80440 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -25,6 +25,7 @@ use codex_core::protocol::PatchApplyEndEvent; use codex_core::protocol::StreamErrorEvent; use codex_core::protocol::TaskCompleteEvent; use codex_core::protocol::TaskStartedEvent; +use codex_protocol::mcp_protocol::ConversationId; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyModifiers; @@ -35,11 +36,10 @@ use std::io::BufRead; use std::io::BufReader; use std::path::PathBuf; use tokio::sync::mpsc::unbounded_channel; -use uuid::Uuid; fn test_config() -> Config { // Use base defaults to avoid depending on host state. - codex_core::config::Config::load_from_base_config_with_overrides( + Config::load_from_base_config_with_overrides( ConfigToml::default(), ConfigOverrides::default(), std::env::temp_dir(), @@ -79,7 +79,7 @@ fn final_answer_without_newline_is_flushed_immediately() { // Set up a VT100 test terminal to capture ANSI visual output let width: u16 = 80; let height: u16 = 2000; - let viewport = ratatui::layout::Rect::new(0, height - 1, width, 1); + let viewport = Rect::new(0, height - 1, width, 1); let backend = ratatui::backend::TestBackend::new(width, height); let mut terminal = crate::custom_terminal::Terminal::with_options(backend) .expect("failed to construct terminal"); @@ -132,13 +132,15 @@ fn final_answer_without_newline_is_flushed_immediately() { fn resumed_initial_messages_render_history() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(); + let conversation_id = ConversationId::new(); + let configured = codex_core::protocol::SessionConfiguredEvent { - session_id: Uuid::nil(), + session_id: conversation_id, model: "test-model".to_string(), history_log_id: 0, history_entry_count: 0, initial_messages: Some(vec![ - EventMsg::UserMessage(codex_core::protocol::UserMessageEvent { + EventMsg::UserMessage(UserMessageEvent { message: "hello from user".to_string(), kind: Some(InputMessageKind::Plain), }), @@ -185,7 +187,7 @@ async fn helpers_are_available_and_do_not_panic() { ))); let init = ChatWidgetInit { config: cfg, - frame_requester: crate::tui::FrameRequester::test_dummy(), + frame_requester: FrameRequester::test_dummy(), app_event_tx: tx, initial_prompt: None, initial_images: Vec::new(), @@ -208,7 +210,7 @@ fn make_chatwidget_manual() -> ( let cfg = test_config(); let bottom = BottomPane::new(BottomPaneParams { app_event_tx: app_event_tx.clone(), - frame_requester: crate::tui::FrameRequester::test_dummy(), + frame_requester: FrameRequester::test_dummy(), has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), @@ -228,10 +230,10 @@ fn make_chatwidget_manual() -> ( interrupts: InterruptManager::new(), reasoning_buffer: String::new(), full_reasoning_buffer: String::new(), - session_id: None, - frame_requester: crate::tui::FrameRequester::test_dummy(), + conversation_id: None, + frame_requester: FrameRequester::test_dummy(), show_welcome_banner: true, - queued_user_messages: std::collections::VecDeque::new(), + queued_user_messages: VecDeque::new(), suppress_session_configured_redraw: false, }; (widget, rx, op_rx) @@ -367,11 +369,10 @@ fn begin_exec(chat: &mut ChatWidget, call_id: &str, raw_cmd: &str) { // Build the full command vec and parse it using core's parser, // then convert to protocol variants for the event payload. let command = vec!["bash".to_string(), "-lc".to_string(), raw_cmd.to_string()]; - let parsed_cmd: Vec = - codex_core::parse_command::parse_command(&command) - .into_iter() - .map(Into::into) - .collect(); + let parsed_cmd: Vec = codex_core::parse_command::parse_command(&command) + .into_iter() + .map(Into::into) + .collect(); chat.handle_codex_event(Event { id: call_id.to_string(), msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent { @@ -412,7 +413,7 @@ fn active_blob(chat: &ChatWidget) -> String { lines_to_single_string(&lines) } -fn open_fixture(name: &str) -> std::fs::File { +fn open_fixture(name: &str) -> File { // 1) Prefer fixtures within this crate { let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); @@ -620,7 +621,7 @@ async fn binary_size_transcript_snapshot() { // Set up a VT100 test terminal to capture ANSI visual output let width: u16 = 80; let height: u16 = 2000; - let viewport = ratatui::layout::Rect::new(0, height - 1, width, 1); + let viewport = Rect::new(0, height - 1, width, 1); let backend = ratatui::backend::TestBackend::new(width, height); let mut terminal = crate::custom_terminal::Terminal::with_options(backend) .expect("failed to construct terminal"); @@ -805,7 +806,7 @@ fn approval_modal_exec_snapshot() { // Build a chat widget with manual channels to avoid spawning the agent. let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); // Ensure policy allows surfacing approvals explicitly (not strictly required for direct event). - chat.config.approval_policy = codex_core::protocol::AskForApproval::OnRequest; + chat.config.approval_policy = AskForApproval::OnRequest; // Inject an exec approval request to display the approval modal. let ev = ExecApprovalRequestEvent { call_id: "call-approve-cmd".into(), @@ -835,7 +836,7 @@ fn approval_modal_exec_snapshot() { #[test] fn approval_modal_exec_without_reason_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); - chat.config.approval_policy = codex_core::protocol::AskForApproval::OnRequest; + chat.config.approval_policy = AskForApproval::OnRequest; let ev = ExecApprovalRequestEvent { call_id: "call-approve-cmd-noreason".into(), @@ -861,10 +862,10 @@ fn approval_modal_exec_without_reason_snapshot() { #[test] fn approval_modal_patch_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); - chat.config.approval_policy = codex_core::protocol::AskForApproval::OnRequest; + chat.config.approval_policy = AskForApproval::OnRequest; // Build a small changeset and a reason/grant_root to exercise the prompt text. - let mut changes = std::collections::HashMap::new(); + let mut changes = HashMap::new(); changes.insert( PathBuf::from("README.md"), FileChange::Add { @@ -910,7 +911,7 @@ fn interrupt_restores_queued_messages_into_composer() { chat.handle_codex_event(Event { id: "turn-1".into(), msg: EventMsg::TurnAborted(codex_core::protocol::TurnAbortedEvent { - reason: codex_core::protocol::TurnAbortReason::Interrupted, + reason: TurnAbortReason::Interrupted, }), }); @@ -1344,7 +1345,7 @@ fn apply_patch_full_flow_integration_like() { fn apply_patch_untrusted_shows_approval_modal() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); // Ensure approval policy is untrusted (OnRequest) - chat.config.approval_policy = codex_core::protocol::AskForApproval::OnRequest; + chat.config.approval_policy = AskForApproval::OnRequest; // Simulate a patch approval request from backend let mut changes = HashMap::new(); @@ -1363,8 +1364,8 @@ fn apply_patch_untrusted_shows_approval_modal() { }); // Render and ensure the approval modal title is present - let area = ratatui::layout::Rect::new(0, 0, 80, 12); - let mut buf = ratatui::buffer::Buffer::empty(area); + let area = Rect::new(0, 0, 80, 12); + let mut buf = Buffer::empty(area); (&chat).render_ref(area, &mut buf); let mut contains_title = false; @@ -1389,7 +1390,7 @@ fn apply_patch_request_shows_diff_summary() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); // Ensure we are in OnRequest so an approval is surfaced - chat.config.approval_policy = codex_core::protocol::AskForApproval::OnRequest; + chat.config.approval_policy = AskForApproval::OnRequest; // Simulate backend asking to apply a patch adding two lines to README.md let mut changes = HashMap::new(); @@ -1691,7 +1692,7 @@ fn chatwidget_exec_and_status_layout_vt100_snapshot() { let width: u16 = 80; let ui_height: u16 = chat.desired_height(width); let vt_height: u16 = 40; - let viewport = ratatui::layout::Rect::new(0, vt_height - ui_height, width, ui_height); + let viewport = Rect::new(0, vt_height - ui_height, width, ui_height); // Use TestBackend for the terminal (no real ANSI emitted by drawing), // but capture VT100 escape stream for history insertion with a separate writer. @@ -1706,7 +1707,7 @@ fn chatwidget_exec_and_status_layout_vt100_snapshot() { } // 2) Render the ChatWidget UI into an off-screen buffer using WidgetRef directly - let mut ui_buf = ratatui::buffer::Buffer::empty(viewport); + let mut ui_buf = Buffer::empty(viewport); (&chat).render_ref(viewport, &mut ui_buf); // 3) Build VT100 visual from the captured ANSI diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index c8fb59232a..8dd9154c51 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -27,6 +27,7 @@ use codex_core::protocol::McpInvocation; use codex_core::protocol::SandboxPolicy; use codex_core::protocol::SessionConfiguredEvent; use codex_core::protocol::TokenUsage; +use codex_protocol::mcp_protocol::ConversationId; use codex_protocol::parse_command::ParsedCommand; use image::DynamicImage; use image::ImageReader; @@ -49,7 +50,6 @@ use std::time::Duration; use std::time::Instant; use tracing::error; use unicode_width::UnicodeWidthStr; -use uuid::Uuid; #[derive(Clone, Debug)] pub(crate) struct CommandOutput { @@ -821,7 +821,7 @@ pub(crate) fn new_completed_mcp_tool_call( pub(crate) fn new_status_output( config: &Config, usage: &TokenUsage, - session_id: &Option, + session_id: &Option, ) -> PlainHistoryCell { let mut lines: Vec> = Vec::new(); lines.push("/status".magenta().into()); From d84a799ec0c5f6d1fd17410b26505d4ce234198e Mon Sep 17 00:00:00 2001 From: Aleksandr Kondrashov <116561995+aramikuto@users.noreply.github.com> Date: Mon, 8 Sep 2025 23:15:12 +0900 Subject: [PATCH 05/11] docs: fix broken link to the "Memory with AGENTS.md" section in codex/README.md (#3300) Fixes https://github.com/openai/codex/issues/3299 Updated the link in README.md so that it correctly points to the [Memory with AGENTS.md](https://github.com/openai/codex/blob/main/docs/getting-started.md#memory-with-agentsmd) section, ensuring users are directed to the right location. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 668a03acaf..8f7e624eea 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Codex CLI supports a rich set of configuration options, with preferences stored - [CLI usage](./docs/getting-started.md#cli-usage) - [Running with a prompt as input](./docs/getting-started.md#running-with-a-prompt-as-input) - [Example prompts](./docs/getting-started.md#example-prompts) - - [Memory with AGENTS.md](./docs/getting-started.md#memory--project-docs) + - [Memory with AGENTS.md](./docs/getting-started.md#memory-with-agentsmd) - [Configuration](./docs/config.md) - [**Sandbox & approvals**](./docs/sandbox.md) - [**Authentication**](./docs/authentication.md) From 6efb52e545aad2b15c102d51ce1fd174b345021e Mon Sep 17 00:00:00 2001 From: dolan <173844978+dolan-openai@users.noreply.github.com> Date: Mon, 8 Sep 2025 08:12:08 -0700 Subject: [PATCH 06/11] feat(mcp): per-server startup timeout (#3182) Seeing timeouts on certain, slow mcp server starting up when codex is invoked. Before this change, the timeout was a hard-coded 10s. Need the ability to define arbitrary timeouts on a per-server basis. ## Summary of changes - Add startup_timeout_ms to McpServerConfig with 10s default when unset - Use per-server timeout for initialize and tools/list - Introduce ManagedClient to store client and timeout; rename LIST_TOOLS_TIMEOUT to DEFAULT_STARTUP_TIMEOUT - Update docs to document startup_timeout_ms with example and options table --------- Co-authored-by: Matthew Dolan --- codex-rs/core/src/config_types.rs | 4 ++ codex-rs/core/src/mcp_connection_manager.rs | 57 ++++++++++++++------- docs/config.md | 5 ++ 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index 27c2712be7..0722dfc0ea 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -18,6 +18,10 @@ pub struct McpServerConfig { #[serde(default)] pub env: Option>, + + /// Startup timeout in milliseconds for initializing MCP server & initially listing tools. + #[serde(default)] + pub startup_timeout_ms: Option, } #[derive(Deserialize, Debug, Copy, Clone, PartialEq)] diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index b5813a0462..563a4e13e9 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -9,6 +9,7 @@ use std::collections::HashMap; use std::collections::HashSet; use std::ffi::OsString; +use std::sync::Arc; use std::time::Duration; use anyhow::Context; @@ -36,8 +37,8 @@ use crate::config_types::McpServerConfig; const MCP_TOOL_NAME_DELIMITER: &str = "__"; const MAX_TOOL_NAME_LENGTH: usize = 64; -/// Timeout for the `tools/list` request. -const LIST_TOOLS_TIMEOUT: Duration = Duration::from_secs(10); +/// Default timeout for initializing MCP server & initially listing tools. +const DEFAULT_STARTUP_TIMEOUT: Duration = Duration::from_secs(10); /// Map that holds a startup error for every MCP server that could **not** be /// spawned successfully. @@ -81,6 +82,11 @@ struct ToolInfo { tool: Tool, } +struct ManagedClient { + client: Arc, + startup_timeout: Duration, +} + /// A thin wrapper around a set of running [`McpClient`] instances. #[derive(Default)] pub(crate) struct McpConnectionManager { @@ -88,7 +94,7 @@ pub(crate) struct McpConnectionManager { /// /// The server name originates from the keys of the `mcp_servers` map in /// the user configuration. - clients: HashMap>, + clients: HashMap, /// Fully qualified tool name -> tool instance. tools: HashMap, @@ -126,8 +132,15 @@ impl McpConnectionManager { continue; } + let startup_timeout = cfg + .startup_timeout_ms + .map(Duration::from_millis) + .unwrap_or(DEFAULT_STARTUP_TIMEOUT); + join_set.spawn(async move { - let McpServerConfig { command, args, env } = cfg; + let McpServerConfig { + command, args, env, .. + } = cfg; let client_res = McpClient::new_stdio_client( command.into(), args.into_iter().map(OsString::from).collect(), @@ -154,12 +167,15 @@ impl McpConnectionManager { protocol_version: mcp_types::MCP_SCHEMA_VERSION.to_owned(), }; let initialize_notification_params = None; - let timeout = Some(Duration::from_secs(10)); match client - .initialize(params, initialize_notification_params, timeout) + .initialize( + params, + initialize_notification_params, + Some(startup_timeout), + ) .await { - Ok(_response) => (server_name, Ok(client)), + Ok(_response) => (server_name, Ok((client, startup_timeout))), Err(e) => (server_name, Err(e)), } } @@ -168,15 +184,20 @@ impl McpConnectionManager { }); } - let mut clients: HashMap> = - HashMap::with_capacity(join_set.len()); + let mut clients: HashMap = HashMap::with_capacity(join_set.len()); while let Some(res) = join_set.join_next().await { let (server_name, client_res) = res?; // JoinError propagation match client_res { - Ok(client) => { - clients.insert(server_name, std::sync::Arc::new(client)); + Ok((client, startup_timeout)) => { + clients.insert( + server_name, + ManagedClient { + client: Arc::new(client), + startup_timeout, + }, + ); } Err(e) => { errors.insert(server_name, e); @@ -212,6 +233,7 @@ impl McpConnectionManager { .clients .get(server) .ok_or_else(|| anyhow!("unknown MCP server '{server}'"))? + .client .clone(); client @@ -229,21 +251,18 @@ impl McpConnectionManager { /// Query every server for its available tools and return a single map that /// contains **all** tools. Each key is the fully-qualified name for the tool. -async fn list_all_tools( - clients: &HashMap>, -) -> Result> { +async fn list_all_tools(clients: &HashMap) -> Result> { let mut join_set = JoinSet::new(); // Spawn one task per server so we can query them concurrently. This // keeps the overall latency roughly at the slowest server instead of // the cumulative latency. - for (server_name, client) in clients { + for (server_name, managed_client) in clients { let server_name_cloned = server_name.clone(); - let client_clone = client.clone(); + let client_clone = managed_client.client.clone(); + let startup_timeout = managed_client.startup_timeout; join_set.spawn(async move { - let res = client_clone - .list_tools(None, Some(LIST_TOOLS_TIMEOUT)) - .await; + let res = client_clone.list_tools(None, Some(startup_timeout)).await; (server_name_cloned, res) }); } diff --git a/docs/config.md b/docs/config.md index d17eab477d..a7f52249a0 100644 --- a/docs/config.md +++ b/docs/config.md @@ -334,6 +334,8 @@ Defines the list of MCP servers that Codex can consult for tool use. Currently, **Note:** Codex may cache the list of tools and resources from an MCP server so that Codex can include this information in context at startup without spawning all the servers. This is designed to save resources by loading MCP servers lazily. +Each server may set `startup_timeout_ms` to adjust how long Codex waits for it to start and respond to a tools listing. The default is `10_000` (10 seconds). + This config option is comparable to how Claude and Cursor define `mcpServers` in their respective JSON config files, though because Codex uses TOML for its config language, the format is slightly different. For example, the following config in JSON: ```json @@ -358,6 +360,8 @@ Should be represented as follows in `~/.codex/config.toml`: command = "npx" args = ["-y", "mcp-server"] env = { "API_KEY" = "value" } +# Optional: override the default 10s startup timeout +startup_timeout_ms = 20_000 ``` ## shell_environment_policy @@ -574,6 +578,7 @@ Options that are specific to the TUI. | `mcp_servers..command` | string | MCP server launcher command. | | `mcp_servers..args` | array | MCP server args. | | `mcp_servers..env` | map | MCP server env vars. | +| `mcp_servers..startup_timeout_ms` | number | Startup timeout in milliseconds (default: 10_000). Timeout is applied both for initializing MCP server and initially listing tools. | | `model_providers..name` | string | Display name. | | `model_providers..base_url` | string | API base URL. | | `model_providers..env_key` | string | Env var for API key. | From ca46510fd35599f999289288794d62525509e9d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 08:21:17 -0700 Subject: [PATCH 07/11] chore(deps): bump insta from 1.43.1 to 1.43.2 in /codex-rs (#3294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [insta](https://github.com/mitsuhiko/insta) from 1.43.1 to 1.43.2.
Release notes

Sourced from insta's releases.

1.43.2

Release Notes

  • Fix panics when cargo metadata fails to execute or parse (e.g., when cargo is not in PATH or returns invalid output). Now falls back to using the manifest directory as the workspace root. #798 (@​adriangb)
  • Fix clippy uninlined_format_args lint warnings. #801
  • Changed diff line numbers to 1-based indexing. #799
  • Preserve snapshot names with INSTA_GLOB_FILTER. #786
  • Bumped libc crate to 0.2.174, fixing building on musl targets, and increasing the MSRV of insta to 1.64.0 (released Sept 2022). #784
  • Fix clippy 1.88 errors. #783
  • Fix source path in snapshots for non-child workspaces. #778
  • Add lifetime to Selector in redaction iterator. #779

Install cargo-insta 1.43.2

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf
https://github.com/mitsuhiko/insta/releases/download/1.43.2/cargo-insta-installer.sh
| sh

Install prebuilt binaries via powershell script

powershell -ExecutionPolicy ByPass -c "irm
https://github.com/mitsuhiko/insta/releases/download/1.43.2/cargo-insta-installer.ps1
| iex"

Download cargo-insta 1.43.2

File Platform Checksum
cargo-insta-aarch64-apple-darwin.tar.xz Apple Silicon macOS checksum
cargo-insta-x86_64-apple-darwin.tar.xz Intel macOS checksum
cargo-insta-x86_64-pc-windows-msvc.zip x64 Windows checksum
cargo-insta-x86_64-unknown-linux-gnu.tar.xz x64 Linux checksum
cargo-insta-x86_64-unknown-linux-musl.tar.xz x64 MUSL Linux checksum
Changelog

Sourced from insta's changelog.

1.43.2

  • Fix panics when cargo metadata fails to execute or parse (e.g., when cargo is not in PATH or returns invalid output). Now falls back to using the manifest directory as the workspace root. #798 (@​adriangb)
  • Fix clippy uninlined_format_args lint warnings. #801
  • Changed diff line numbers to 1-based indexing. #799
  • Preserve snapshot names with INSTA_GLOB_FILTER. #786
  • Bumped libc crate to 0.2.174, fixing building on musl targets, and increasing the MSRV of insta to 1.64.0 (released Sept 2022). #784
  • Fix clippy 1.88 errors. #783
  • Fix source path in snapshots for non-child workspaces. #778
  • Add lifetime to Selector in redaction iterator. #779
Commits
  • 01fc57f Fix Windows runner configuration for releases
  • 88c9a2f Prepare CHANGELOG for 1.43.2 release (#802)
  • d03c2a6 Improve error handling for cargo workspace detection (#800)
  • 55987ac Fix clippy uninlined_format_args lint warnings (#801)
  • ae26e81 Change diff line numbers to 1-based indexing (#799)
  • 26efb60 Release insta 1.43.2 (#791)
  • 7793782 Preserve snapshot names with INSTA_GLOB_FILTER (#786)
  • 1d6e0c7 chore: bump libc crate (#784)
  • 1a17ea9 chore: fix clippy 1.88 errors (#783)
  • 7d0de48 Fix source path in snapshots for non-child workspaces (#778)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=insta&package-manager=cargo&previous-version=1.43.1&new-version=1.43.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- codex-rs/Cargo.lock | 4 ++-- codex-rs/tui/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index eda83be76b..ef9e8c4458 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2434,9 +2434,9 @@ checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "insta" -version = "1.43.1" +version = "1.43.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" +checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" dependencies = [ "console", "once_cell", diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 9a115d28b0..486a545548 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -100,7 +100,7 @@ arboard = "3" [dev-dependencies] chrono = { version = "0.4", features = ["serde"] } -insta = "1.43.1" +insta = "1.43.2" pretty_assertions = "1" rand = "0.9" vt100 = "0.16.2" From 6b878bea01b9cec2889975afa1d734e128241599 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 08:22:59 -0700 Subject: [PATCH 08/11] chore(deps): bump tree-sitter from 0.25.8 to 0.25.9 in /codex-rs (#3295) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [tree-sitter](https://github.com/tree-sitter/tree-sitter) from 0.25.8 to 0.25.9.
Release notes

Sourced from tree-sitter's releases.

v0.25.9

What's Changed

New Contributors

Full Changelog: https://github.com/tree-sitter/tree-sitter/compare/v0.25.8...v0.25.9

Commits
  • a467ea8 fix(rust): correct crate versions in root Cargo.toml file
  • 6cd25aa 0.25.9
  • 027136c fix(generate): use correct state id when adding terminal states to
  • 14c4d2f fix(generate): return error when single state transitions have
  • 8e2b5ad fix(test): improve readability of corpus error message mismatch
  • bb82b94 fix(web): correct type errors, improve build
  • 59f3cb9 fix(npm): add directory to repository fields
  • a80cd86 fix(cli): fix DSL type declarations
  • 253003c fix(generate): warn users when extra rule can lead to parser hang
  • e61407c fix(bindings): properly detect MSVC compiler
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tree-sitter&package-manager=cargo&previous-version=0.25.8&new-version=0.25.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- codex-rs/Cargo.lock | 4 ++-- codex-rs/apply-patch/Cargo.toml | 2 +- codex-rs/core/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index ef9e8c4458..3ea78081e5 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -5354,9 +5354,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.25.8" +version = "0.25.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7b8994f367f16e6fa14b5aebbcb350de5d7cbea82dc5b00ae997dd71680dd2" +checksum = "ccd2a058a86cfece0bf96f7cce1021efef9c8ed0e892ab74639173e5ed7a34fa" dependencies = [ "cc", "regex", diff --git a/codex-rs/apply-patch/Cargo.toml b/codex-rs/apply-patch/Cargo.toml index fc8c57aa8b..7b5919a323 100644 --- a/codex-rs/apply-patch/Cargo.toml +++ b/codex-rs/apply-patch/Cargo.toml @@ -18,7 +18,7 @@ workspace = true anyhow = "1" similar = "2.7.0" thiserror = "2.0.16" -tree-sitter = "0.25.8" +tree-sitter = "0.25.9" tree-sitter-bash = "0.25.0" once_cell = "1" diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 3d2cf4a68d..b5c7537ece 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -53,7 +53,7 @@ tokio-util = "0.7.16" toml = "0.9.5" toml_edit = "0.23.4" tracing = { version = "0.1.41", features = ["log"] } -tree-sitter = "0.25.8" +tree-sitter = "0.25.9" tree-sitter-bash = "0.25.0" uuid = { version = "1", features = ["serde", "v4"] } whoami = "1.6.1" From e47bd336897302a7e071d1dd1cfb2417a124f563 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 08:24:36 -0700 Subject: [PATCH 09/11] chore(deps): bump clap from 4.5.45 to 4.5.47 in /codex-rs (#3296) Bumps [clap](https://github.com/clap-rs/clap) from 4.5.45 to 4.5.47.
Release notes

Sourced from clap's releases.

v4.5.47

[4.5.47] - 2025-09-02

Features

  • Added impl FromArgMatches for ()
  • Added impl Args for ()
  • Added impl Subcommand for ()
  • Added impl FromArgMatches for Infallible
  • Added impl Subcommand for Infallible

Fixes

  • (derive) Update runtime error text to match clap

v4.5.46

[4.5.46] - 2025-08-26

Features

  • Expose StyledStr::push_str
Changelog

Sourced from clap's changelog.

[4.5.47] - 2025-09-02

Features

  • Added impl FromArgMatches for ()
  • Added impl Args for ()
  • Added impl Subcommand for ()
  • Added impl FromArgMatches for Infallible
  • Added impl Subcommand for Infallible

Fixes

  • (derive) Update runtime error text to match clap

[4.5.46] - 2025-08-26

Features

  • Expose StyledStr::push_str
Commits
  • f046ca6 chore: Release
  • 436949d docs: Update changelog
  • 1ddab84 Merge pull request #5954 from epage/tests
  • 8a66dbf test(complete): Add more native cases
  • 76465cf test(complete): Make things more consistent
  • 232cedb test(complete): Remove redundant index
  • 02244a6 Merge pull request #5949 from krobelus/option-name-completions-after-positionals
  • 2e13847 fix(complete): Missing options in multi-val arg
  • 74388d7 test(complete): Multi-valued, unbounded positional
  • 5b3d45f refactor(complete): Extract function for options
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=clap&package-manager=cargo&previous-version=4.5.45&new-version=4.5.47)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- codex-rs/Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 3ea78081e5..4a5e7cfa73 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -547,9 +547,9 @@ checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" [[package]] name = "clap" -version = "4.5.45" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" dependencies = [ "clap_builder", "clap_derive", @@ -557,9 +557,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.44" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" dependencies = [ "anstream", "anstyle", @@ -579,9 +579,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck", "proc-macro2", From e2b3053b2b9c39fd9a83ec3172318db64d31dc56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 08:25:23 -0700 Subject: [PATCH 10/11] chore(deps): bump image from 0.25.6 to 0.25.8 in /codex-rs (#3297) Bumps [image](https://github.com/image-rs/image) from 0.25.6 to 0.25.8.
Changelog

Sourced from image's changelog.

Version 0.25.8

Re-release of 0.25.7

Fixes:

  • Reverted a signature change to load_from_memory that lead to large scale type inference breakage despite being technically compatible.
  • Color conversion Luma to Rgb used incorrect coefficients instead of broadcasting.

Version 0.25.7 (yanked)

Features:

  • Added an API for external image format implementations to register themselves as decoders for a specific format in image (#2372)
  • Added CICP awarenes via moxcms to support color spaces (#2531). The support for transforming is limited for now and will be gradually expanded.
  • You can now embed Exif metadata when writing JPEG, PNG and WebP images (#2537, #2539)
  • Added functions to extract orientation from Exif metadata and optionally clear it in the Exif chunk (#2484)
  • Serde support for more types (#2445)
  • PNM encoder now supports writing 16-bit images (#2431)

API improvements:

  • save, save_with_format, write_to and write_with_encoder methods on DynamicImage now automatically convert the pixel format when necessary instead of returning an error (#2501)
  • Added DynamicImage::has_alpha() convenience method
  • Implemented TryFrom<ExtendedColorType> for ColorType (#2444)
  • Added const HAS_ALPHA to trait Pixel
  • Unified the error for unsupported encoder colors (#2543)
  • Added a hooks module to customize builtin behavior, register_format_detection_hook and register_decoding_hook for the determining format of a file and selecting an ImageDecoder implementation respectively. (#2372)

Performance improvements:

  • Gaussian blur (#2496) and box blur (#2515) are now faster
  • Improve compilation times by avoiding unnecessary instantiation of generic functions (#2468, #2470)

Bug fixes:

  • Many improvements to image format decoding: TIFF, WebP, AVIF, PNG, GIF, BMP, TGA
  • Fixed GifEncoder::encode() ignoring the speed parameter and always using the slowest speed (#2504)
  • .pnm is now recognized as a file extension for the PNM format (#2559)
Commits
  • 98b001d Merge pull request #2592 from image-rs/release-0.25.8
  • f862320 Metadata and changelog for a 0.25.8
  • 3b1c1db Merge pull request #2593 from image-rs/luma-to-rgb-transform-is-broadcast
  • 1f574d3 Replace manual rounding code with f32::round
  • 545cb37 Color tests in the middle of dynamic range
  • 9882fa9 Remove coefficients from luma_expand
  • 70b9aa3 Revert "Make load_from_memory generic"
  • b94c333 Enable CI for backport branch
  • a24556b Merge pull request #2581 from image-rs/release-0.25.7
  • 9175dbc Fix readme typo (#2580)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=image&package-manager=cargo&previous-version=0.25.6&new-version=0.25.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- codex-rs/Cargo.lock | 65 ++++++++++++++++++++++++++++++++--------- codex-rs/tui/Cargo.toml | 2 +- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 4a5e7cfa73..385c6321bb 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1707,6 +1707,26 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "fd-lock" version = "4.0.4" @@ -2361,9 +2381,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.6" +version = "0.25.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" dependencies = [ "bytemuck", "byteorder-lite", @@ -2371,6 +2391,7 @@ dependencies = [ "exr", "gif", "image-webp", + "moxcms", "num-traits", "png", "qoi", @@ -2624,12 +2645,6 @@ dependencies = [ "libc", ] -[[package]] -name = "jpeg-decoder" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" - [[package]] name = "js-sys" version = "0.3.77" @@ -2928,6 +2943,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "moxcms" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "multimap" version = "0.10.1" @@ -3436,11 +3461,11 @@ dependencies = [ [[package]] name = "png" -version = "0.17.16" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.1", "crc32fast", "fdeflate", "flate2", @@ -3609,6 +3634,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" +[[package]] +name = "pxfm" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55f4fedc84ed39cb7a489322318976425e42a147e2be79d8f878e2884f94e84" +dependencies = [ + "num-traits", +] + [[package]] name = "qoi" version = "0.4.1" @@ -4976,13 +5010,16 @@ dependencies = [ [[package]] name = "tiff" -version = "0.9.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" dependencies = [ + "fax", "flate2", - "jpeg-decoder", + "half", + "quick-error", "weezl", + "zune-jpeg", ] [[package]] diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 486a545548..42fb1b0f4f 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -44,7 +44,7 @@ crossterm = { version = "0.28.1", features = [ "event-stream", ] } diffy = "0.4.2" -image = { version = "^0.25.6", default-features = false, features = [ +image = { version = "^0.25.8", default-features = false, features = [ "jpeg", "png", ] } From bb4f97d8e9ca559a72e8786de01d1c3d25c3b21a Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 8 Sep 2025 08:30:21 -0700 Subject: [PATCH 11/11] chore: upgrade to actions/setup-node@v5 --- .github/workflows/ci.yml | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e62d7bca56..4dfd7596d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,33 +14,18 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 10.8.1 run_install: false - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "store_path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - - - name: Setup pnpm cache - uses: actions/cache@v4 + - name: Setup Node.js + uses: actions/setup-node@v5 with: - path: ${{ steps.pnpm-cache.outputs.store_path }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- + node-version: 22 - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile # Run all tasks using workspace filters