Support asynchronous command hooks (#37533)

## Why

Hook configurations can mark command handlers as asynchronous, but Codex previously skipped those handlers outside `SessionEnd`.

## What changed

- Run asynchronous command hooks in the background with a per-session concurrency limit, while keeping `SessionEnd` hooks synchronous.
- Prevent asynchronous hooks from blocking, stopping, rewriting, or otherwise controlling the operation that launched them.
- Deliver warnings and additional context at safe turn boundaries: inject results into an active turn after sampling, or buffer them ahead of the next user prompt when the session is idle.
- Preserve in-flight hooks across configuration reloads, scope spilled output to the thread, and abort outstanding work during session shutdown.

## Testing

Add unit and integration coverage for background scheduling, concurrency, output parsing, active and idle result delivery, configuration reloads, and shutdown cleanup.

GitOrigin-RevId: 8094552e4afe7b47b09a61bb575bb20f4e491d8d
This commit is contained in:
Abhinav authored and copyberry committed 2026-08-08 04:24:12 +00:00
1 parent 61a3dd4387
commit 6f647caa9b
29 files changed
+1514 -429

No files matched your search

+7 -6
View File
@@ -1742,7 +1742,7 @@ impl Session {
self.emit_config_changed_contributors(previous_config.as_ref(), new_config.as_ref());
self.schedule_mcp_prewarm();
let environments = self.services.turn_environments.snapshot().await;
let hooks = build_hooks_for_config(
let hooks_config = build_hooks_config(
config.as_ref(),
self.services.plugins_manager.as_ref(),
environments.single_local_environment(),
@@ -1756,6 +1756,7 @@ impl Session {
&state.session_configuration.original_config_do_not_use,
&config,
) {
let hooks = self.hooks().reconfigured(hooks_config);
self.services.hooks.store(Arc::new(hooks));
}
}
@@ -4203,12 +4204,12 @@ pub(crate) fn emit_subagent_session_started(
});
}
/// Builds the hook engine for one config snapshot, including any enabled plugin hooks.
async fn build_hooks_for_config(
/// Builds hook configuration for one config snapshot, including any enabled plugin hooks.
async fn build_hooks_config(
config: &Config,
plugins_manager: &PluginsManager,
environment: Option<&TurnEnvironment>,
) -> Hooks {
) -> HooksConfig {
let (hook_shell_program, hook_shell_argv) = environment
.and_then(|environment| environment.shell.as_ref())
.map(|shell| {
@@ -4222,7 +4223,7 @@ async fn build_hooks_for_config(
let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await;
let plugin_hook_sources = plugin_outcome.effective_plugin_hook_sources();
let plugin_hook_load_warnings = plugin_outcome.effective_plugin_hook_warnings();
Hooks::new(HooksConfig {
HooksConfig {
legacy_notify_argv: config.notify.clone(),
feature_enabled: config.features.enabled(Feature::CodexHooks),
bypass_hook_trust: config.bypass_hook_trust,
@@ -4231,7 +4232,7 @@ async fn build_hooks_for_config(
plugin_hook_load_warnings,
shell_program: hook_shell_program,
shell_args: hook_shell_argv,
})
}
}
#[cfg(test)]