From 13be504063ed94a494267eefb7a24cc452c58c83 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Tue, 5 May 2026 10:34:44 -0700 Subject: [PATCH] revert legacy notify deprecation (#21152) # Why Revert #20524 for now because the computer use plugin has not migrated off legacy `notify` yet. Keeping the deprecation in place today would show users a warning before the plugin path is ready to move, so this rolls the change back until that migration is complete. # What - revert the legacy `notify` deprecation change from #20524 - restore the prior `notify` behavior and remove the temporary deprecation metrics/docs from that change Once the computer use plugin has migrated, we can land the same deprecation again. --- codex-rs/README.md | 2 +- codex-rs/config/src/config_toml.rs | 2 +- codex-rs/core/config.schema.json | 2 +- codex-rs/core/src/config/mod.rs | 4 +- codex-rs/core/src/session/session.rs | 22 --- codex-rs/core/src/session/turn.rs | 8 - .../core/tests/suite/deprecation_notice.rs | 32 ---- codex-rs/hooks/src/user_notification.rs | 153 ++++++++++++++++++ codex-rs/otel/src/metrics/names.rs | 2 - 9 files changed, 158 insertions(+), 69 deletions(-) create mode 100644 codex-rs/hooks/src/user_notification.rs diff --git a/codex-rs/README.md b/codex-rs/README.md index 2cc3a6b8f1..d219061a35 100644 --- a/codex-rs/README.md +++ b/codex-rs/README.md @@ -46,7 +46,7 @@ Use `codex mcp` to add/list/get/remove MCP server launchers defined in `config.t ### Notifications -The legacy `notify` setting is deprecated and will be removed in a future release. Existing configurations still work, but new automation should use lifecycle hooks instead. The [notify documentation](../docs/config.md#notify) explains the remaining compatibility behavior. When Codex detects that it is running under WSL 2 inside Windows Terminal (`WT_SESSION` is set), the TUI automatically falls back to native Windows toast notifications so approval prompts and completed turns surface even though Windows Terminal does not implement OSC 9. +You can enable notifications by configuring a script that is run whenever the agent finishes a turn. The [notify documentation](../docs/config.md#notify) includes a detailed example that explains how to get desktop notifications via [terminal-notifier](https://github.com/julienXX/terminal-notifier) on macOS. When Codex detects that it is running under WSL 2 inside Windows Terminal (`WT_SESSION` is set), the TUI automatically falls back to native Windows toast notifications so approval prompts and completed turns surface even though Windows Terminal does not implement OSC 9. ### `codex exec` to run Codex programmatically/non-interactively diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 6406f9e1ff..89eb30b798 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -146,7 +146,7 @@ pub struct ConfigToml { #[serde(default)] pub permissions: Option, - /// Deprecated optional external command to spawn for end-user notifications. + /// Optional external command to spawn for end-user notifications. #[serde(default)] pub notify: Option>, diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 824f542b7e..acc64f14a3 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -4294,7 +4294,7 @@ }, "notify": { "default": null, - "description": "Deprecated optional external command to spawn for end-user notifications.", + "description": "Optional external command to spawn for end-user notifications.", "items": { "type": "string" }, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 2154bb0170..3692ded0f9 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -480,7 +480,7 @@ pub struct Config { /// - `Some("...")`: use the provided attribution text verbatim pub commit_attribution: Option, - /// Deprecated optional external notifier command. When set, Codex will spawn this + /// Optional external notifier command. When set, Codex will spawn this /// program after each completed *turn* (i.e. when the agent finishes /// processing a user submission). The value must be the full command /// broken into argv tokens **without** the trailing JSON argument - Codex @@ -499,7 +499,7 @@ pub struct Config { /// notify-send Codex '{"type":"agent-turn-complete","turn-id":"12345"}' /// ``` /// - /// If unset the feature is disabled. Use lifecycle hooks for new automation. + /// If unset the feature is disabled. pub notify: Option>, /// TUI notification settings, including enabled events, delivery method, and focus condition. diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 2444ded83f..0bb8da6ea6 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1,6 +1,5 @@ use super::*; use crate::goals::GoalRuntimeState; -use codex_otel::LEGACY_NOTIFY_CONFIGURED_METRIC; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::protocol::TurnEnvironmentSelection; @@ -573,24 +572,6 @@ impl Session { }), }); } - let legacy_notify_configured = config - .notify - .as_ref() - .is_some_and(|argv| !argv.is_empty() && !argv[0].is_empty()); - if legacy_notify_configured { - post_session_configured_events.push(Event { - id: INITIAL_SUBMIT_ID.to_owned(), - msg: EventMsg::DeprecationNotice(DeprecationNoticeEvent { - summary: - "`notify` is deprecated and will be removed in a future release." - .to_string(), - details: Some( - "Switch to a `Stop` hook for end-of-turn automation. See https://developers.openai.com/codex/hooks." - .to_string(), - ), - }), - }); - } for message in &config.startup_warnings { post_session_configured_events.push(Event { id: "".to_owned(), @@ -648,9 +629,6 @@ impl Session { if let Some(service_name) = session_configuration.metrics_service_name.as_deref() { session_telemetry = session_telemetry.with_metrics_service_name(service_name); } - if legacy_notify_configured { - session_telemetry.counter(LEGACY_NOTIFY_CONFIGURED_METRIC, /*inc*/ 1, &[]); - } let network_proxy_audit_metadata = NetworkProxyAuditMetadata { conversation_id: Some(conversation_id.to_string()), app_version: Some(env!("CARGO_PKG_VERSION").to_string()), diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index ce035b2cea..8ebbb0a8ac 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -72,7 +72,6 @@ use codex_hooks::HookEvent; use codex_hooks::HookEventAfterAgent; use codex_hooks::HookPayload; use codex_hooks::HookResult; -use codex_otel::LEGACY_NOTIFY_RUN_METRIC; use codex_protocol::config_types::ModeKind; use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; @@ -580,13 +579,6 @@ pub(crate) async fn run_turn( }, }) .await; - if !hook_outcomes.is_empty() { - turn_context.session_telemetry.counter( - LEGACY_NOTIFY_RUN_METRIC, - /*inc*/ 1, - &[], - ); - } let mut abort_message = None; for hook_outcome in hook_outcomes { diff --git a/codex-rs/core/tests/suite/deprecation_notice.rs b/codex-rs/core/tests/suite/deprecation_notice.rs index 52041fe453..0ef7ddc339 100644 --- a/codex-rs/core/tests/suite/deprecation_notice.rs +++ b/codex-rs/core/tests/suite/deprecation_notice.rs @@ -115,38 +115,6 @@ async fn emits_deprecation_notice_for_experimental_instructions_file() -> anyhow Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn emits_deprecation_notice_for_notify() -> anyhow::Result<()> { - skip_if_no_network!(Ok(())); - - let server = start_mock_server().await; - - let mut builder = test_codex().with_config(|config| { - config.notify = Some(vec!["notify-send".to_string(), "Codex".to_string()]); - }); - - let TestCodex { codex, .. } = builder.build(&server).await?; - - let notice = wait_for_event_match(&codex, |event| match event { - EventMsg::DeprecationNotice(ev) if ev.summary.contains("`notify`") => Some(ev.clone()), - _ => None, - }) - .await; - - let DeprecationNoticeEvent { summary, details } = notice; - assert_eq!( - summary, - "`notify` is deprecated and will be removed in a future release.".to_string(), - ); - assert_eq!( - details.as_deref(), - Some( - "Switch to a `Stop` hook for end-of-turn automation. See https://developers.openai.com/codex/hooks." - ), - ); - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn emits_deprecation_notice_for_web_search_feature_flag_values() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/hooks/src/user_notification.rs b/codex-rs/hooks/src/user_notification.rs new file mode 100644 index 0000000000..97af09a3b9 --- /dev/null +++ b/codex-rs/hooks/src/user_notification.rs @@ -0,0 +1,153 @@ +use std::process::Stdio; +use std::sync::Arc; + +use serde::Serialize; + +use crate::Hook; +use crate::HookEvent; +use crate::HookPayload; +use crate::HookResult; +use crate::command_from_argv; + +/// Legacy notify payload appended as the final argv argument for backward compatibility. +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(tag = "type", rename_all = "kebab-case")] +enum UserNotification { + #[serde(rename_all = "kebab-case")] + AgentTurnComplete { + thread_id: String, + turn_id: String, + cwd: String, + #[serde(skip_serializing_if = "Option::is_none")] + client: Option, + + /// Messages that the user sent to the agent to initiate the turn. + input_messages: Vec, + + /// The last message sent by the assistant in the turn. + last_assistant_message: Option, + }, +} + +pub fn legacy_notify_json(payload: &HookPayload) -> Result { + match &payload.hook_event { + HookEvent::AfterAgent { event } => { + serde_json::to_string(&UserNotification::AgentTurnComplete { + thread_id: event.thread_id.to_string(), + turn_id: event.turn_id.clone(), + cwd: payload.cwd.display().to_string(), + client: payload.client.clone(), + input_messages: event.input_messages.clone(), + last_assistant_message: event.last_assistant_message.clone(), + }) + } + _ => Err(serde_json::Error::io(std::io::Error::other( + "legacy notify payload is only supported for after_agent", + ))), + } +} + +pub fn notify_hook(argv: Vec) -> Hook { + let argv = Arc::new(argv); + Hook { + name: "legacy_notify".to_string(), + func: Arc::new(move |payload: &HookPayload| { + let argv = Arc::clone(&argv); + Box::pin(async move { + let mut command = match command_from_argv(&argv) { + Some(command) => command, + None => return HookResult::Success, + }; + if let Ok(notify_payload) = legacy_notify_json(payload) { + command.arg(notify_payload); + } + + // Backwards-compat: match legacy notify behavior (argv + JSON arg, fire-and-forget). + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + match command.spawn() { + Ok(_) => HookResult::Success, + Err(err) => HookResult::FailedContinue(err.into()), + } + }) + }), + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use codex_protocol::ThreadId; + use codex_utils_absolute_path::test_support::PathBufExt; + use codex_utils_absolute_path::test_support::test_path_buf; + use pretty_assertions::assert_eq; + use serde_json::Value; + use serde_json::json; + + use super::*; + + fn expected_notification_json() -> Value { + let cwd = test_path_buf("/Users/example/project"); + json!({ + "type": "agent-turn-complete", + "thread-id": "b5f6c1c2-1111-2222-3333-444455556666", + "turn-id": "12345", + "cwd": cwd.display().to_string(), + "client": "codex-tui", + "input-messages": ["Rename `foo` to `bar` and update the callsites."], + "last-assistant-message": "Rename complete and verified `cargo build` succeeds.", + }) + } + + #[test] + fn test_user_notification() -> Result<()> { + let notification = UserNotification::AgentTurnComplete { + thread_id: "b5f6c1c2-1111-2222-3333-444455556666".to_string(), + turn_id: "12345".to_string(), + cwd: test_path_buf("/Users/example/project") + .display() + .to_string(), + client: Some("codex-tui".to_string()), + input_messages: vec!["Rename `foo` to `bar` and update the callsites.".to_string()], + last_assistant_message: Some( + "Rename complete and verified `cargo build` succeeds.".to_string(), + ), + }; + let serialized = serde_json::to_string(¬ification)?; + let actual: Value = serde_json::from_str(&serialized)?; + assert_eq!(actual, expected_notification_json()); + Ok(()) + } + + #[test] + fn legacy_notify_json_matches_historical_wire_shape() -> Result<()> { + let payload = HookPayload { + session_id: ThreadId::new(), + cwd: test_path_buf("/Users/example/project").abs(), + client: Some("codex-tui".to_string()), + triggered_at: chrono::Utc::now(), + hook_event: HookEvent::AfterAgent { + event: crate::HookEventAfterAgent { + thread_id: ThreadId::from_string("b5f6c1c2-1111-2222-3333-444455556666") + .expect("valid thread id"), + turn_id: "12345".to_string(), + input_messages: vec![ + "Rename `foo` to `bar` and update the callsites.".to_string(), + ], + last_assistant_message: Some( + "Rename complete and verified `cargo build` succeeds.".to_string(), + ), + }, + }, + }; + + let serialized = legacy_notify_json(&payload)?; + let actual: Value = serde_json::from_str(&serialized)?; + assert_eq!(actual, expected_notification_json()); + + Ok(()) + } +} diff --git a/codex-rs/otel/src/metrics/names.rs b/codex-rs/otel/src/metrics/names.rs index dc49372168..b2f2d639ce 100644 --- a/codex-rs/otel/src/metrics/names.rs +++ b/codex-rs/otel/src/metrics/names.rs @@ -37,8 +37,6 @@ pub const CURATED_PLUGINS_STARTUP_SYNC_METRIC: &str = "codex.plugins.startup_syn pub const CURATED_PLUGINS_STARTUP_SYNC_FINAL_METRIC: &str = "codex.plugins.startup_sync.final"; pub const HOOK_RUN_METRIC: &str = "codex.hooks.run"; pub const HOOK_RUN_DURATION_METRIC: &str = "codex.hooks.run.duration_ms"; -pub const LEGACY_NOTIFY_CONFIGURED_METRIC: &str = "codex.notify.configured"; -pub const LEGACY_NOTIFY_RUN_METRIC: &str = "codex.notify.run"; /// Total runtime of a startup prewarm attempt until it completes, tagged by final status. pub const STARTUP_PREWARM_DURATION_METRIC: &str = "codex.startup_prewarm.duration_ms"; /// Age of the startup prewarm attempt when the first real turn resolves it, tagged by outcome.