diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 0330f6d0f4..61ae601171 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1074,12 +1074,16 @@ impl Session { InitialHistory::New | InitialHistory::Forked(_) ) ); + let should_run_session_start = + !matches!(session_configuration.session_source, SessionSource::SubAgent(_)); // record_initial_history can emit events. We record only after the SessionConfiguredEvent is emitted. sess.record_initial_history(initial_history).await; { let mut state = sess.state.lock().await; - state.set_pending_session_start_source(Some(session_start_source)); + state.set_pending_session_start_source( + should_run_session_start.then_some(session_start_source), + ); state.set_pending_subagent_start(should_run_subagent_start); } diff --git a/codex-rs/core/tests/suite/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index 633b9f00be..2992031045 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -114,6 +114,21 @@ fn write_home_skill(codex_home: &Path, dir: &str, name: &str, description: &str) } fn write_subagent_lifecycle_hooks(home: &Path, stop_prompts: &[&str]) -> Result<()> { + let session_start_script_path = home.join("session_start_hook.py"); + let session_start_log_path = home.join("session_start_hook_log.jsonl"); + let session_start_script = format!( + r#"import json +from pathlib import Path +import sys + +log_path = Path(r"{session_start_log_path}") +payload = json.load(sys.stdin) +with log_path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(payload) + "\n") +"#, + session_start_log_path = session_start_log_path.display(), + ); + let start_script_path = home.join("subagent_start_hook.py"); let start_log_path = home.join("subagent_start_hook_log.jsonl"); let start_script = format!( @@ -177,6 +192,13 @@ print(json.dumps({{"systemMessage": "root stop complete"}})) let hooks = serde_json::json!({ "hooks": { + "SessionStart": [{ + "matcher": "startup", + "hooks": [{ + "type": "command", + "command": format!("python3 {}", session_start_script_path.display()), + }] + }], "SubagentStart": [{ "matcher": "worker", "hooks": [{ @@ -200,6 +222,7 @@ print(json.dumps({{"systemMessage": "root stop complete"}})) } }); + fs::write(&session_start_script_path, session_start_script)?; fs::write(&start_script_path, start_script)?; fs::write(&subagent_stop_script_path, subagent_stop_script)?; fs::write(&stop_script_path, stop_script)?; @@ -485,6 +508,15 @@ async fn subagent_start_injects_context_once_for_child() -> Result<()> { Some(spawned_id.as_str()) ); + let session_start_inputs = + read_hook_log(test.codex_home_path(), "session_start_hook_log.jsonl")?; + assert_eq!(session_start_inputs.len(), 1); + assert_eq!(session_start_inputs[0]["source"].as_str(), Some("startup")); + assert_ne!( + session_start_inputs[0]["session_id"].as_str(), + Some(spawned_id.as_str()) + ); + Ok(()) }