mirror of
https://github.com/openai/codex.git
synced 2026-05-05 13:51:29 +03:00
## Why The `notify` hook payload did not identify which Codex client started the turn. That meant downstream notification hooks could not distinguish between completions coming from the TUI and completions coming from app-server clients such as VS Code or Xcode. Now that the Codex App provides its own desktop notifications, it would be nice to be able to filter those out. This change adds that context without changing the existing payload shape for callers that do not know the client name, and keeps the new end-to-end test cross-platform. ## What changed - added an optional top-level `client` field to the legacy `notify` JSON payload - threaded that value through `core` and `hooks`; the internal session and turn state now carries it as `app_server_client_name` - set the field to `codex-tui` for TUI turns - captured `initialize.clientInfo.name` in the app server and applied it to subsequent turns before dispatching hooks - replaced the notify integration test hook with a `python3` script so the test does not rely on Unix shell permissions or `bash` - documented the new field in `docs/config.md` ## Testing - `cargo test -p codex-hooks` - `cargo test -p codex-tui` - `cargo test -p codex-app-server suite::v2::initialize::turn_start_notify_payload_includes_initialize_client_name -- --exact --nocapture` - `cargo test -p codex-core` (`src/lib.rs` passed; `core/tests/all.rs` still has unrelated existing failures in this environment) ## Docs The public config reference on `developers.openai.com/codex` should mention that the legacy `notify` payload may include a top-level `client` field. The TUI reports `codex-tui`, and the app server reports `initialize.clientInfo.name` when it is available.
149 lines
5.2 KiB
Rust
149 lines
5.2 KiB
Rust
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 pretty_assertions::assert_eq;
|
|
use serde_json::Value;
|
|
use serde_json::json;
|
|
|
|
use super::*;
|
|
|
|
fn expected_notification_json() -> Value {
|
|
json!({
|
|
"type": "agent-turn-complete",
|
|
"thread-id": "b5f6c1c2-1111-2222-3333-444455556666",
|
|
"turn-id": "12345",
|
|
"cwd": "/Users/example/project",
|
|
"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: "/Users/example/project".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: std::path::Path::new("/Users/example/project").to_path_buf(),
|
|
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(())
|
|
}
|
|
}
|