[codex] Add SessionStart hook parity outside CLI (#9970)

Co-authored-by: Max Novich <mnovich@Maxs-MacBook-Pro.local>
This commit is contained in:
Max Novich
2026-06-30 14:15:46 -07:00
committed by GitHub
parent 40bfb137ad
commit d308b13e75
2 changed files with 86 additions and 7 deletions
-7
View File
@@ -487,10 +487,6 @@ impl CliSession {
/// Start an interactive session, optionally with an initial message
pub async fn interactive(&mut self, prompt: Option<String>) -> Result<()> {
self.agent
.emit_hook(goose::hooks::HookEvent::SessionStart, &self.session_id)
.await;
let result = self.run_interactive(prompt).await;
self.agent
@@ -1139,9 +1135,6 @@ impl CliSession {
/// Process a single message and exit
pub async fn headless(&mut self, prompt: String) -> Result<()> {
self.agent
.emit_hook(goose::hooks::HookEvent::SessionStart, &self.session_id)
.await;
let message = Message::user().with_text(&prompt);
let result = self
.process_message(message, CancellationToken::default(), false)
+86
View File
@@ -1569,6 +1569,19 @@ impl Agent {
let message_text = user_message.as_concat_text();
let session = session_manager
.get_session(&session_config.id, true)
.await?;
let is_first_turn = session
.conversation
.as_ref()
.map(|conversation| conversation.messages().is_empty())
.unwrap_or(true);
if is_first_turn {
self.emit_hook(crate::hooks::HookEvent::SessionStart, &session_config.id)
.await;
}
if self
.hook_manager
.has_hooks(crate::hooks::HookEvent::UserPromptSubmit)
@@ -3482,6 +3495,64 @@ exit 0
}
}
struct SessionStartHookTestEnv {
temp_dir: TempDir,
hook_log: PathBuf,
}
impl SessionStartHookTestEnv {
fn new() -> Result<Self> {
let temp_dir = tempfile::tempdir()?;
let plugin_dir = temp_dir.path().join("session-start");
std::fs::create_dir_all(plugin_dir.join("hooks"))?;
std::fs::write(
plugin_dir.join("hooks/hooks.json"),
r#"{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "sh ${PLUGIN_ROOT}/start.sh" }
]
}
]
}
}
"#,
)?;
std::fs::write(
plugin_dir.join("start.sh"),
r#"#!/bin/sh
echo start >> "$PLUGIN_ROOT/hook.log"
"#,
)?;
Ok(Self {
temp_dir,
hook_log: plugin_dir.join("hook.log"),
})
}
fn hook_manager(&self) -> crate::hooks::HookManager {
crate::hooks::HookManager::from_plugins_for_test(vec![DiscoveredPlugin {
name: "session-start".into(),
root: self.temp_dir.path().join("session-start"),
scope: PluginScope::Project,
}])
}
fn data_dir(&self) -> PathBuf {
self.temp_dir.path().join("data")
}
fn hook_invocations(&self) -> usize {
std::fs::read_to_string(&self.hook_log)
.unwrap_or_default()
.lines()
.count()
}
}
struct CountingTextProvider {
call_count: AtomicUsize,
}
@@ -3694,6 +3765,21 @@ exit 0
.collect()
}
#[tokio::test]
async fn session_start_hook_emits_once_for_first_reply_turn() -> Result<()> {
let env = SessionStartHookTestEnv::new()?;
let provider = Arc::new(CountingTextProvider::new());
let (agent, session_id) =
create_test_agent(env.data_dir(), env.hook_manager(), provider.clone()).await?;
run_stop_hook_test_turn(&agent, &session_id, "first").await?;
run_stop_hook_test_turn(&agent, &session_id, "second").await?;
assert_eq!(env.hook_invocations(), 1);
assert_eq!(provider.call_count(), 2);
Ok(())
}
#[tokio::test]
async fn stop_hook_block_cap_allows_configured_consecutive_blocks_then_overrides() -> Result<()>
{