From fbfbfe5fc57a9f232b09866ae0eae6e792377074 Mon Sep 17 00:00:00 2001 From: Andrei Eternal Date: Tue, 12 May 2026 19:05:10 -0700 Subject: [PATCH] hooks: use new session IDs instead of thread IDs for hooks, apply parent's session ID to subagents' hooks (#22268) ## Why hook semantics treat `session_id` as shared across a root session and its subagents. Codex hooks were still emitting the current thread ID, which made spawned agents look like independent sessions and made it harder for hook integrations to correlate work across a root thread and its spawned helpers This change makes hooks use Codex's existing shared session identity so hook `session_id` matches the root-thread session across spawned subagents. ## What Changed - switch hook payloads to use the existing shared session identity from core instead of the current thread ID - cover all hook surfaces that expose `session_id`, including `SessionStart`, tool hooks, compact hooks, prompt-submit hooks, stop hooks, and legacy after-agent dispatch --- codex-rs/core/src/hook_runtime.rs | 14 +++++++------- codex-rs/core/src/mcp_tool_call_tests.rs | 4 ++-- codex-rs/core/src/session/turn.rs | 4 ++-- codex-rs/core/src/tools/registry.rs | 1 - 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/codex-rs/core/src/hook_runtime.rs b/codex-rs/core/src/hook_runtime.rs index 175813a544..3d72e4a7c6 100644 --- a/codex-rs/core/src/hook_runtime.rs +++ b/codex-rs/core/src/hook_runtime.rs @@ -115,7 +115,7 @@ pub(crate) async fn run_pending_session_start_hooks( }; let request = codex_hooks::SessionStartRequest { - session_id: sess.conversation_id, + session_id: sess.session_id().into(), cwd: turn_context.cwd.clone(), transcript_path: sess.hook_transcript_path().await, model: turn_context.model_info.slug.clone(), @@ -148,7 +148,7 @@ pub(crate) async fn run_pre_tool_use_hooks( tool_input: &Value, ) -> PreToolUseHookResult { let request = PreToolUseRequest { - session_id: sess.conversation_id, + session_id: sess.session_id().into(), turn_id: turn_context.sub_id.clone(), cwd: turn_context.cwd.clone(), transcript_path: sess.hook_transcript_path().await, @@ -207,7 +207,7 @@ pub(crate) async fn run_permission_request_hooks( payload: PermissionRequestPayload, ) -> Option { let request = PermissionRequestRequest { - session_id: sess.conversation_id, + session_id: sess.session_id().into(), turn_id: turn_context.sub_id.clone(), cwd: turn_context.cwd.to_path_buf(), transcript_path: sess.hook_transcript_path().await, @@ -247,7 +247,7 @@ pub(crate) async fn run_post_tool_use_hooks( tool_response: Value, ) -> PostToolUseOutcome { let request = PostToolUseRequest { - session_id: sess.conversation_id, + session_id: sess.session_id().into(), turn_id: turn_context.sub_id.clone(), cwd: turn_context.cwd.clone(), transcript_path: sess.hook_transcript_path().await, @@ -274,7 +274,7 @@ pub(crate) async fn run_pre_compact_hooks( trigger: CompactionTrigger, ) -> PreCompactHookOutcome { let request = codex_hooks::PreCompactRequest { - session_id: sess.conversation_id, + session_id: sess.session_id().into(), turn_id: turn_context.sub_id.clone(), cwd: turn_context.cwd.clone(), transcript_path: sess.hook_transcript_path().await, @@ -311,7 +311,7 @@ pub(crate) async fn run_post_compact_hooks( trigger: CompactionTrigger, ) -> PostCompactHookOutcome { let request = codex_hooks::PostCompactRequest { - session_id: sess.conversation_id, + session_id: sess.session_id().into(), turn_id: turn_context.sub_id.clone(), cwd: turn_context.cwd.clone(), transcript_path: sess.hook_transcript_path().await, @@ -336,7 +336,7 @@ pub(crate) async fn run_user_prompt_submit_hooks( prompt: String, ) -> HookRuntimeOutcome { let request = UserPromptSubmitRequest { - session_id: sess.conversation_id, + session_id: sess.session_id().into(), turn_id: turn_context.sub_id.clone(), cwd: turn_context.cwd.clone(), transcript_path: sess.hook_transcript_path().await, diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 326e0cfe31..ecad2091c5 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -2331,7 +2331,7 @@ async fn permission_request_hook_allows_mcp_tool_call() { assert_eq!( inputs, vec![serde_json::json!({ - "session_id": session.conversation_id, + "session_id": session.session_id(), "turn_id": "turn_id", "cwd": turn_context.cwd, "transcript_path": null, @@ -2391,7 +2391,7 @@ async fn permission_request_hook_uses_hook_tool_name_without_metadata() { assert_eq!( inputs, vec![serde_json::json!({ - "session_id": session.conversation_id, + "session_id": session.session_id(), "turn_id": "turn_id", "cwd": turn_context.cwd, "transcript_path": null, diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 541ebc4b81..87334d1ac7 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -523,7 +523,7 @@ pub(crate) async fn run_turn( } .to_string(); let stop_request = codex_hooks::StopRequest { - session_id: sess.conversation_id, + session_id: sess.session_id().into(), turn_id: turn_context.sub_id.clone(), cwd: turn_context.cwd.clone(), transcript_path: sess.hook_transcript_path().await, @@ -573,7 +573,7 @@ pub(crate) async fn run_turn( let hook_outcomes = sess .hooks() .dispatch(HookPayload { - session_id: sess.conversation_id, + session_id: sess.session_id().into(), cwd: turn_context.cwd.clone(), client: turn_context.app_server_client_name.clone(), triggered_at: chrono::Utc::now(), diff --git a/codex-rs/core/src/tools/registry.rs b/codex-rs/core/src/tools/registry.rs index 95e7d17ce5..65dcc863a9 100644 --- a/codex-rs/core/src/tools/registry.rs +++ b/codex-rs/core/src/tools/registry.rs @@ -628,7 +628,6 @@ fn unsupported_tool_call_message(payload: &ToolPayload, tool_name: &ToolName) -> _ => format!("unsupported call: {tool_name}"), } } - #[cfg(test)] #[path = "registry_tests.rs"] mod tests;