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.
This commit is contained in:
Abhinav
2026-05-05 10:34:44 -07:00
committed by GitHub
Unverified
parent 394242e95b
commit 13be504063
9 changed files with 158 additions and 69 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -146,7 +146,7 @@ pub struct ConfigToml {
#[serde(default)]
pub permissions: Option<PermissionsToml>,
/// Deprecated optional external command to spawn for end-user notifications.
/// Optional external command to spawn for end-user notifications.
#[serde(default)]
pub notify: Option<Vec<String>>,
+1 -1
View File
@@ -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"
},
+2 -2
View File
@@ -480,7 +480,7 @@ pub struct Config {
/// - `Some("...")`: use the provided attribution text verbatim
pub commit_attribution: Option<String>,
/// 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<Vec<String>>,
/// TUI notification settings, including enabled events, delivery method, and focus condition.
-22
View File
@@ -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()),
-8
View File
@@ -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 {
@@ -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(()));
+153
View File
@@ -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<String>,
/// Messages that the user sent to the agent to initiate the turn.
input_messages: Vec<String>,
/// The last message sent by the assistant in the turn.
last_assistant_message: Option<String>,
},
}
pub fn legacy_notify_json(payload: &HookPayload) -> Result<String, serde_json::Error> {
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<String>) -> 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(&notification)?;
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(())
}
}
-2
View File
@@ -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.