diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index a2e49fc5..5025c82e 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -843,6 +843,7 @@ dependencies = [ "codex-backend-client", "codex-common", "codex-core", + "codex-feedback", "codex-file-search", "codex-login", "codex-protocol", diff --git a/codex-rs/app-server-protocol/src/protocol.rs b/codex-rs/app-server-protocol/src/protocol.rs index ab7341a2..ca4be39e 100644 --- a/codex-rs/app-server-protocol/src/protocol.rs +++ b/codex-rs/app-server-protocol/src/protocol.rs @@ -124,6 +124,13 @@ client_request_definitions! { response: GetAccountRateLimitsResponse, }, + #[serde(rename = "feedback/upload")] + #[ts(rename = "feedback/upload")] + UploadFeedback { + params: UploadFeedbackParams, + response: UploadFeedbackResponse, + }, + #[serde(rename = "account/read")] #[ts(rename = "account/read")] GetAccount { @@ -384,6 +391,23 @@ pub struct ListModelsResponse { pub next_cursor: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct UploadFeedbackParams { + pub classification: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub conversation_id: Option, + pub include_logs: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct UploadFeedbackResponse { + pub thread_id: String, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(tag = "type")] #[ts(tag = "type")] diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index c1efb2ef..d693e7bb 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -24,6 +24,7 @@ codex-file-search = { workspace = true } codex-login = { workspace = true } codex-protocol = { workspace = true } codex-app-server-protocol = { workspace = true } +codex-feedback = { workspace = true } codex-utils-json-to-toml = { workspace = true } chrono = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index b2b242fb..59dd0f63 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -52,6 +52,8 @@ use codex_app_server_protocol::ServerRequestPayload; use codex_app_server_protocol::SessionConfiguredNotification; use codex_app_server_protocol::SetDefaultModelParams; use codex_app_server_protocol::SetDefaultModelResponse; +use codex_app_server_protocol::UploadFeedbackParams; +use codex_app_server_protocol::UploadFeedbackResponse; use codex_app_server_protocol::UserInfoResponse; use codex_app_server_protocol::UserSavedConfig; use codex_backend_client::Client as BackendClient; @@ -85,6 +87,7 @@ use codex_core::protocol::EventMsg; use codex_core::protocol::ExecApprovalRequestEvent; use codex_core::protocol::Op; use codex_core::protocol::ReviewDecision; +use codex_feedback::CodexFeedback; use codex_login::ServerOptions as LoginServerOptions; use codex_login::ShutdownHandle; use codex_login::run_login_server; @@ -136,6 +139,7 @@ pub(crate) struct CodexMessageProcessor { // Queue of pending interrupt requests per conversation. We reply when TurnAborted arrives. pending_interrupts: Arc>>>, pending_fuzzy_searches: Arc>>>, + feedback: CodexFeedback, } impl CodexMessageProcessor { @@ -145,6 +149,7 @@ impl CodexMessageProcessor { outgoing: Arc, codex_linux_sandbox_exe: Option, config: Arc, + feedback: CodexFeedback, ) -> Self { Self { auth_manager, @@ -156,6 +161,7 @@ impl CodexMessageProcessor { active_login: Arc::new(Mutex::new(None)), pending_interrupts: Arc::new(Mutex::new(HashMap::new())), pending_fuzzy_searches: Arc::new(Mutex::new(HashMap::new())), + feedback, } } @@ -275,6 +281,9 @@ impl CodexMessageProcessor { } => { self.get_account_rate_limits(request_id).await; } + ClientRequest::UploadFeedback { request_id, params } => { + self.upload_feedback(request_id, params).await; + } } } @@ -1418,6 +1427,77 @@ impl CodexMessageProcessor { let response = FuzzyFileSearchResponse { files: results }; self.outgoing.send_response(request_id, response).await; } + + async fn upload_feedback(&self, request_id: RequestId, params: UploadFeedbackParams) { + let UploadFeedbackParams { + classification, + reason, + conversation_id, + include_logs, + } = params; + + let snapshot = self.feedback.snapshot(conversation_id); + let thread_id = snapshot.thread_id.clone(); + + let validated_rollout_path = if include_logs { + match conversation_id { + Some(conv_id) => self.resolve_rollout_path(conv_id).await, + None => None, + } + } else { + None + }; + + let upload_result = tokio::task::spawn_blocking(move || { + let rollout_path_ref = validated_rollout_path.as_deref(); + snapshot.upload_feedback( + &classification, + reason.as_deref(), + include_logs, + rollout_path_ref, + ) + }) + .await; + + let upload_result = match upload_result { + Ok(result) => result, + Err(join_err) => { + let error = JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: format!("failed to upload feedback: {join_err}"), + data: None, + }; + self.outgoing.send_error(request_id, error).await; + return; + } + }; + + match upload_result { + Ok(()) => { + let response = UploadFeedbackResponse { thread_id }; + self.outgoing.send_response(request_id, response).await; + } + Err(err) => { + let error = JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: format!("failed to upload feedback: {err}"), + data: None, + }; + self.outgoing.send_error(request_id, error).await; + } + } + } + + async fn resolve_rollout_path(&self, conversation_id: ConversationId) -> Option { + match self + .conversation_manager + .get_conversation(conversation_id) + .await + { + Ok(conv) => Some(conv.rollout_path()), + Err(_) => None, + } + } } async fn apply_bespoke_event_handling( diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index c4102f82..6ef98691 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -12,16 +12,19 @@ use crate::message_processor::MessageProcessor; use crate::outgoing_message::OutgoingMessage; use crate::outgoing_message::OutgoingMessageSender; use codex_app_server_protocol::JSONRPCMessage; +use codex_feedback::CodexFeedback; use tokio::io::AsyncBufReadExt; use tokio::io::AsyncWriteExt; use tokio::io::BufReader; use tokio::io::{self}; use tokio::sync::mpsc; +use tracing::Level; use tracing::debug; use tracing::error; use tracing::info; use tracing_subscriber::EnvFilter; use tracing_subscriber::Layer; +use tracing_subscriber::filter::Targets; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; @@ -82,6 +85,8 @@ pub async fn run_main( std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}")) })?; + let feedback = CodexFeedback::new(); + let otel = codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION")).map_err(|e| { std::io::Error::new( @@ -96,8 +101,15 @@ pub async fn run_main( .with_writer(std::io::stderr) .with_filter(EnvFilter::from_default_env()); + let feedback_layer = tracing_subscriber::fmt::layer() + .with_writer(feedback.make_writer()) + .with_ansi(false) + .with_target(false) + .with_filter(Targets::new().with_default(Level::TRACE)); + let _ = tracing_subscriber::registry() .with(stderr_fmt) + .with(feedback_layer) .with(otel.as_ref().map(|provider| { OpenTelemetryTracingBridge::new(&provider.logger).with_filter( tracing_subscriber::filter::filter_fn(codex_core::otel_init::codex_export_filter), @@ -112,6 +124,7 @@ pub async fn run_main( outgoing_message_sender, codex_linux_sandbox_exe, std::sync::Arc::new(config), + feedback.clone(), ); async move { while let Some(msg) = incoming_rx.recv().await { diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 15086c19..a2c192cf 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -17,6 +17,7 @@ use codex_core::ConversationManager; use codex_core::config::Config; use codex_core::default_client::USER_AGENT_SUFFIX; use codex_core::default_client::get_codex_user_agent; +use codex_feedback::CodexFeedback; use codex_protocol::protocol::SessionSource; use std::sync::Arc; @@ -33,6 +34,7 @@ impl MessageProcessor { outgoing: OutgoingMessageSender, codex_linux_sandbox_exe: Option, config: Arc, + feedback: CodexFeedback, ) -> Self { let outgoing = Arc::new(outgoing); let auth_manager = AuthManager::shared(config.codex_home.clone(), false); @@ -46,6 +48,7 @@ impl MessageProcessor { outgoing.clone(), codex_linux_sandbox_exe, config, + feedback, ); Self { diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs index f6d35b8d..90c7645d 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -30,6 +30,7 @@ use codex_app_server_protocol::SendUserMessageParams; use codex_app_server_protocol::SendUserTurnParams; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::SetDefaultModelParams; +use codex_app_server_protocol::UploadFeedbackParams; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCMessage; @@ -242,6 +243,15 @@ impl McpProcess { self.send_request("account/rateLimits/read", None).await } + /// Send a `feedback/upload` JSON-RPC request. + pub async fn send_upload_feedback_request( + &mut self, + params: UploadFeedbackParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("feedback/upload", params).await + } + /// Send a `userInfo` JSON-RPC request. pub async fn send_user_info_request(&mut self) -> anyhow::Result { self.send_request("userInfo", None).await diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index d89ce5f8..a7134c9c 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -20,7 +20,6 @@ use async_channel::Sender; use codex_apply_patch::ApplyPatchAction; use codex_protocol::ConversationId; use codex_protocol::items::TurnItem; -use codex_protocol::protocol::ConversationPathResponseEvent; use codex_protocol::protocol::ExitedReviewModeEvent; use codex_protocol::protocol::ItemCompletedEvent; use codex_protocol::protocol::ItemStartedEvent; @@ -598,6 +597,19 @@ impl Session { self.tx_event.clone() } + /// Ensure all rollout writes are durably flushed. + pub(crate) async fn flush_rollout(&self) { + let recorder = { + let guard = self.services.rollout.lock().await; + guard.clone() + }; + if let Some(rec) = recorder + && let Err(e) = rec.flush().await + { + warn!("failed to flush rollout recorder: {e}"); + } + } + fn next_internal_sub_id(&self) -> String { let id = self .next_internal_sub_id @@ -612,6 +624,8 @@ impl Session { // Build and record initial items (user instructions + environment context) let items = self.build_initial_context(&turn_context); self.record_conversation_items(&turn_context, &items).await; + // Ensure initial items are visible to immediate readers (e.g., tests, forks). + self.flush_rollout().await; } InitialHistory::Resumed(_) | InitialHistory::Forked(_) => { let rollout_items = conversation_history.get_rollout_items(); @@ -628,6 +642,8 @@ impl Session { if persist && !rollout_items.is_empty() { self.persist_rollout_items(&rollout_items).await; } + // Flush after seeding history and any persisted rollout copy. + self.flush_rollout().await; } } } @@ -1401,33 +1417,7 @@ async fn submission_loop(sess: Arc, config: Arc, rx_sub: Receiv sess.send_event_raw(event).await; break; } - Op::GetPath => { - let sub_id = sub.id.clone(); - // Flush rollout writes before returning the path so readers observe a consistent file. - let (path, rec_opt) = { - let guard = sess.services.rollout.lock().await; - match guard.as_ref() { - Some(rec) => (rec.get_rollout_path(), Some(rec.clone())), - None => { - error!("rollout recorder not found"); - continue; - } - } - }; - if let Some(rec) = rec_opt - && let Err(e) = rec.flush().await - { - warn!("failed to flush rollout recorder before GetHistory: {e}"); - } - let event = Event { - id: sub_id.clone(), - msg: EventMsg::ConversationPath(ConversationPathResponseEvent { - conversation_id: sess.conversation_id, - path, - }), - }; - sess.send_event_raw(event).await; - } + Op::Review { review_request } => { let turn_context = sess .new_turn_with_sub_id(sub.id.clone(), SessionSettingsUpdate::default()) @@ -2231,6 +2221,8 @@ pub(crate) async fn exit_review_mode( }], ) .await; + // Make the recorded review note visible immediately for readers. + session.flush_rollout().await; } fn mcp_init_error_display( diff --git a/codex-rs/core/src/codex_conversation.rs b/codex-rs/core/src/codex_conversation.rs index d3b00046..5bb9c97c 100644 --- a/codex-rs/core/src/codex_conversation.rs +++ b/codex-rs/core/src/codex_conversation.rs @@ -3,16 +3,21 @@ use crate::error::Result as CodexResult; use crate::protocol::Event; use crate::protocol::Op; use crate::protocol::Submission; +use std::path::PathBuf; pub struct CodexConversation { codex: Codex, + rollout_path: PathBuf, } /// Conduit for the bidirectional stream of messages that compose a conversation /// in Codex. impl CodexConversation { - pub(crate) fn new(codex: Codex) -> Self { - Self { codex } + pub(crate) fn new(codex: Codex, rollout_path: PathBuf) -> Self { + Self { + codex, + rollout_path, + } } pub async fn submit(&self, op: Op) -> CodexResult { @@ -27,4 +32,8 @@ impl CodexConversation { pub async fn next_event(&self) -> CodexResult { self.codex.next_event().await } + + pub fn rollout_path(&self) -> PathBuf { + self.rollout_path.clone() + } } diff --git a/codex-rs/core/src/conversation_manager.rs b/codex-rs/core/src/conversation_manager.rs index a30038f2..b911526f 100644 --- a/codex-rs/core/src/conversation_manager.rs +++ b/codex-rs/core/src/conversation_manager.rs @@ -98,7 +98,10 @@ impl ConversationManager { } }; - let conversation = Arc::new(CodexConversation::new(codex)); + let conversation = Arc::new(CodexConversation::new( + codex, + session_configured.rollout_path.clone(), + )); self.conversations .write() .await diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index ea2954fa..64688de9 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -72,7 +72,6 @@ pub(crate) fn should_persist_event_msg(ev: &EventMsg) -> bool { | EventMsg::PlanUpdate(_) | EventMsg::ShutdownComplete | EventMsg::ViewImageToolCall(_) - | EventMsg::ConversationPath(_) | EventMsg::ItemStarted(_) | EventMsg::ItemCompleted(_) => false, } diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index 95f5d479..93b614f9 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -267,10 +267,6 @@ impl RolloutRecorder { })) } - pub(crate) fn get_rollout_path(&self) -> PathBuf { - self.rollout_path.clone() - } - pub async fn shutdown(&self) -> std::io::Result<()> { let (tx_done, rx_done) = oneshot::channel(); match self.tx.send(RolloutCmd::Shutdown { ack: tx_done }).await { diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index 4261a305..cea03ee7 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -18,7 +18,6 @@ use codex_core::built_in_model_providers; use codex_core::codex::compact::SUMMARIZATION_PROMPT; use codex_core::config::Config; use codex_core::config::OPENAI_DEFAULT_MODEL; -use codex_core::protocol::ConversationPathResponseEvent; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; @@ -61,7 +60,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() { user_turn(&base, "hello world").await; compact_conversation(&base).await; user_turn(&base, "AFTER_COMPACT").await; - let base_path = fetch_conversation_path(&base, "base conversation").await; + let base_path = fetch_conversation_path(&base).await; assert!( base_path.exists(), "compact+resume test expects base path {base_path:?} to exist", @@ -69,7 +68,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() { let resumed = resume_conversation(&manager, &config, base_path).await; user_turn(&resumed, "AFTER_RESUME").await; - let resumed_path = fetch_conversation_path(&resumed, "resumed conversation").await; + let resumed_path = fetch_conversation_path(&resumed).await; assert!( resumed_path.exists(), "compact+resume test expects resumed path {resumed_path:?} to exist", @@ -518,7 +517,7 @@ async fn compact_resume_after_second_compaction_preserves_history() { user_turn(&base, "hello world").await; compact_conversation(&base).await; user_turn(&base, "AFTER_COMPACT").await; - let base_path = fetch_conversation_path(&base, "base conversation").await; + let base_path = fetch_conversation_path(&base).await; assert!( base_path.exists(), "second compact test expects base path {base_path:?} to exist", @@ -526,7 +525,7 @@ async fn compact_resume_after_second_compaction_preserves_history() { let resumed = resume_conversation(&manager, &config, base_path).await; user_turn(&resumed, "AFTER_RESUME").await; - let resumed_path = fetch_conversation_path(&resumed, "resumed conversation").await; + let resumed_path = fetch_conversation_path(&resumed).await; assert!( resumed_path.exists(), "second compact test expects resumed path {resumed_path:?} to exist", @@ -537,7 +536,7 @@ async fn compact_resume_after_second_compaction_preserves_history() { compact_conversation(&forked).await; user_turn(&forked, "AFTER_COMPACT_2").await; - let forked_path = fetch_conversation_path(&forked, "forked conversation").await; + let forked_path = fetch_conversation_path(&forked).await; assert!( forked_path.exists(), "second compact test expects forked path {forked_path:?} to exist", @@ -792,22 +791,8 @@ async fn compact_conversation(conversation: &Arc) { wait_for_event(conversation, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; } -async fn fetch_conversation_path( - conversation: &Arc, - context: &str, -) -> std::path::PathBuf { - conversation - .submit(Op::GetPath) - .await - .expect("request conversation path"); - match wait_for_event(conversation, |ev| { - matches!(ev, EventMsg::ConversationPath(_)) - }) - .await - { - EventMsg::ConversationPath(ConversationPathResponseEvent { path, .. }) => path, - _ => panic!("expected ConversationPath event for {context}"), - } +async fn fetch_conversation_path(conversation: &Arc) -> std::path::PathBuf { + conversation.rollout_path() } async fn resume_conversation( diff --git a/codex-rs/core/tests/suite/fork_conversation.rs b/codex-rs/core/tests/suite/fork_conversation.rs index da2ff8c3..75b37ae7 100644 --- a/codex-rs/core/tests/suite/fork_conversation.rs +++ b/codex-rs/core/tests/suite/fork_conversation.rs @@ -4,7 +4,6 @@ use codex_core::ModelProviderInfo; use codex_core::NewConversation; use codex_core::built_in_model_providers; use codex_core::parse_turn_item; -use codex_core::protocol::ConversationPathResponseEvent; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_core::protocol::RolloutItem; @@ -79,13 +78,7 @@ async fn fork_conversation_twice_drops_to_first_message() { } // Request history from the base conversation to obtain rollout path. - codex.submit(Op::GetPath).await.unwrap(); - let base_history = - wait_for_event(&codex, |ev| matches!(ev, EventMsg::ConversationPath(_))).await; - let base_path = match &base_history { - EventMsg::ConversationPath(ConversationPathResponseEvent { path, .. }) => path.clone(), - _ => panic!("expected ConversationHistory event"), - }; + let base_path = codex.rollout_path(); // GetHistory flushes before returning the path; no wait needed. @@ -140,15 +133,7 @@ async fn fork_conversation_twice_drops_to_first_message() { .await .expect("fork 1"); - codex_fork1.submit(Op::GetPath).await.unwrap(); - let fork1_history = wait_for_event(&codex_fork1, |ev| { - matches!(ev, EventMsg::ConversationPath(_)) - }) - .await; - let fork1_path = match &fork1_history { - EventMsg::ConversationPath(ConversationPathResponseEvent { path, .. }) => path.clone(), - _ => panic!("expected ConversationHistory event after first fork"), - }; + let fork1_path = codex_fork1.rollout_path(); // GetHistory on fork1 flushed; the file is ready. let fork1_items = read_items(&fork1_path); @@ -166,15 +151,7 @@ async fn fork_conversation_twice_drops_to_first_message() { .await .expect("fork 2"); - codex_fork2.submit(Op::GetPath).await.unwrap(); - let fork2_history = wait_for_event(&codex_fork2, |ev| { - matches!(ev, EventMsg::ConversationPath(_)) - }) - .await; - let fork2_path = match &fork2_history { - EventMsg::ConversationPath(ConversationPathResponseEvent { path, .. }) => path.clone(), - _ => panic!("expected ConversationHistory event after second fork"), - }; + let fork2_path = codex_fork2.rollout_path(); // GetHistory on fork2 flushed; the file is ready. let fork1_items = read_items(&fork1_path); let fork1_user_inputs = find_user_input_positions(&fork1_items); diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index 422acd92..5577de7c 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -7,7 +7,6 @@ use codex_core::REVIEW_PROMPT; use codex_core::ResponseItem; use codex_core::built_in_model_providers; use codex_core::config::Config; -use codex_core::protocol::ConversationPathResponseEvent; use codex_core::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG; use codex_core::protocol::EventMsg; use codex_core::protocol::ExitedReviewModeEvent; @@ -120,13 +119,7 @@ async fn review_op_emits_lifecycle_and_review_output() { // Also verify that a user message with the header and a formatted finding // was recorded back in the parent session's rollout. - codex.submit(Op::GetPath).await.unwrap(); - let history_event = - wait_for_event(&codex, |ev| matches!(ev, EventMsg::ConversationPath(_))).await; - let path = match history_event { - EventMsg::ConversationPath(ConversationPathResponseEvent { path, .. }) => path, - other => panic!("expected ConversationPath event, got {other:?}"), - }; + let path = codex.rollout_path(); let text = std::fs::read_to_string(&path).expect("read rollout file"); let mut saw_header = false; @@ -482,13 +475,7 @@ async fn review_input_isolated_from_parent_history() { assert_eq!(instructions, REVIEW_PROMPT); // Also verify that a user interruption note was recorded in the rollout. - codex.submit(Op::GetPath).await.unwrap(); - let history_event = - wait_for_event(&codex, |ev| matches!(ev, EventMsg::ConversationPath(_))).await; - let path = match history_event { - EventMsg::ConversationPath(ConversationPathResponseEvent { path, .. }) => path, - other => panic!("expected ConversationPath event, got {other:?}"), - }; + let path = codex.rollout_path(); let text = std::fs::read_to_string(&path).expect("read rollout file"); let mut saw_interruption_message = false; for line in text.lines() { 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 b07cd16d..bd37a438 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -510,7 +510,6 @@ impl EventProcessor for EventProcessorWithHumanOutput { } }, EventMsg::ShutdownComplete => return CodexStatus::Shutdown, - EventMsg::ConversationPath(_) => {} EventMsg::UserMessage(_) => {} EventMsg::EnteredReviewMode(_) => {} EventMsg::ExitedReviewMode(_) => {} diff --git a/codex-rs/feedback/src/lib.rs b/codex-rs/feedback/src/lib.rs index b1adb3db..e1ccc3aa 100644 --- a/codex-rs/feedback/src/lib.rs +++ b/codex-rs/feedback/src/lib.rs @@ -172,7 +172,6 @@ impl CodexLogSnapshot { &self, classification: &str, reason: Option<&str>, - cli_version: &str, include_logs: bool, rollout_path: Option<&std::path::Path>, ) -> Result<()> { @@ -198,6 +197,7 @@ impl CodexLogSnapshot { ..Default::default() }); + let cli_version = env!("CARGO_PKG_VERSION"); let mut tags = BTreeMap::from([ (String::from("thread_id"), self.thread_id.to_string()), (String::from("classification"), classification.to_string()), diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index a6af754d..9f826f4e 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -281,7 +281,6 @@ async fn run_codex_tool_session_inner( | EventMsg::GetHistoryEntryResponse(_) | EventMsg::PlanUpdate(_) | EventMsg::TurnAborted(_) - | EventMsg::ConversationPath(_) | EventMsg::UserMessage(_) | EventMsg::ShutdownComplete | EventMsg::ViewImageToolCall(_) diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 2334561e..3cf16bb8 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -166,10 +166,6 @@ pub enum Op { /// Request a single history entry identified by `log_id` + `offset`. GetHistoryEntryRequest { offset: usize, log_id: u64 }, - /// Request the full in-memory conversation transcript for the current session. - /// Reply is delivered via `EventMsg::ConversationHistory`. - GetPath, - /// Request the list of MCP tools available across all configured servers. /// Reply is delivered via `EventMsg::McpListToolsResponse`. ListMcpTools, @@ -519,8 +515,6 @@ pub enum EventMsg { /// Notification that the agent is shutting down. ShutdownComplete, - ConversationPath(ConversationPathResponseEvent), - /// Entered review mode. EnteredReviewMode(ReviewRequest), diff --git a/codex-rs/tui/src/app_backtrack.rs b/codex-rs/tui/src/app_backtrack.rs index b3e948ec..e2ded3d3 100644 --- a/codex-rs/tui/src/app_backtrack.rs +++ b/codex-rs/tui/src/app_backtrack.rs @@ -103,9 +103,16 @@ impl App { nth_user_message: usize, ) { self.backtrack.pending = Some((base_id, nth_user_message, prefill)); - self.app_event_tx.send(crate::app_event::AppEvent::CodexOp( - codex_core::protocol::Op::GetPath, - )); + if let Some(path) = self.chat_widget.rollout_path() { + let ev = ConversationPathResponseEvent { + conversation_id: base_id, + path, + }; + self.app_event_tx + .send(crate::app_event::AppEvent::ConversationHistory(ev)); + } else { + tracing::error!("rollout path unavailable; cannot backtrack"); + } } /// Open transcript overlay (enters alternate screen and shows full transcript). diff --git a/codex-rs/tui/src/bottom_pane/feedback_view.rs b/codex-rs/tui/src/bottom_pane/feedback_view.rs index 234dbbd2..0d42c0be 100644 --- a/codex-rs/tui/src/bottom_pane/feedback_view.rs +++ b/codex-rs/tui/src/bottom_pane/feedback_view.rs @@ -73,13 +73,11 @@ impl FeedbackNoteView { let rollout_path_ref = self.rollout_path.as_deref(); let classification = feedback_classification(self.category); - let cli_version = crate::version::CODEX_CLI_VERSION; let mut thread_id = self.snapshot.thread_id.clone(); let result = self.snapshot.upload_feedback( classification, reason_opt, - cli_version, self.include_logs, if self.include_logs { rollout_path_ref diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index fdebe158..64dc89d8 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1516,10 +1516,6 @@ impl ChatWidget { self.on_user_message_event(ev); } } - EventMsg::ConversationPath(ev) => { - self.app_event_tx - .send(crate::app_event::AppEvent::ConversationHistory(ev)); - } EventMsg::EnteredReviewMode(review_request) => { self.on_entered_review_mode(review_request) } @@ -2250,6 +2246,10 @@ impl ChatWidget { self.conversation_id } + pub(crate) fn rollout_path(&self) -> Option { + self.current_rollout_path.clone() + } + /// Return a reference to the widget's current config (includes any /// runtime overrides applied via TUI, e.g., model or approval policy). pub(crate) fn config_ref(&self) -> &Config {