mirror of
https://github.com/openai/codex.git
synced 2026-10-25 07:55:34 +00:00
Add hooks for interrupted turns (#40511)
## What changed - Add an `Interrupt` hook event that runs for an active top-level turn before its interrupted abort event is emitted. - Flush the turn transcript before invoking the hook and provide the session, turn, transcript, working directory, model, and permission mode in its input. - Support command and MCP handlers, including asynchronous commands, with a one-second default timeout and a three-second maximum. - Expose the event through hook configuration, managed requirements, app-server notifications, generated schemas, analytics, and the TUI hook views. ## Testing - Cover handler discovery, timeout normalization, output parsing, protocol compatibility, TUI rendering, and interrupt execution ordering. GitOrigin-RevId: 163fa7c098d94ac2775f6d137f8e916f8ea9b6eb
This commit is contained in:
committed by
copyberry
parent
9b2ef38f54
commit
cbfd999db7
@@ -6,6 +6,7 @@ use codex_analytics::CompactionTrigger;
|
||||
use codex_analytics::HookRunFact;
|
||||
use codex_analytics::build_track_events_context;
|
||||
use codex_core_plugins::executor_plugin_hook_sources;
|
||||
use codex_hooks::InterruptRequest;
|
||||
use codex_hooks::PermissionRequestDecision;
|
||||
use codex_hooks::PermissionRequestOutcome;
|
||||
use codex_hooks::PermissionRequestRequest;
|
||||
@@ -427,6 +428,35 @@ pub(crate) async fn run_session_end_hooks(sess: &Arc<Session>) {
|
||||
emit_hook_completed_events(sess, &turn_context, outcome.hook_events).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn run_turn_interrupt_hooks(sess: &Arc<Session>, turn_context: &Arc<TurnContext>) {
|
||||
if matches!(&turn_context.session_source, SessionSource::SubAgent(_)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hooks = sess.hooks();
|
||||
let preview_runs = hooks.preview_interrupt();
|
||||
if preview_runs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let request = InterruptRequest {
|
||||
session_id: sess.session_id().into(),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.clone(),
|
||||
transcript_path: sess.hook_transcript_path().await,
|
||||
model: turn_context.model_info.slug.clone(),
|
||||
permission_mode: hook_permission_mode(turn_context),
|
||||
};
|
||||
if let Err(err) = sess.flush_rollout().await {
|
||||
tracing::warn!("failed to flush transcript before Interrupt hook: {err}");
|
||||
}
|
||||
emit_hook_started_events(sess, turn_context, preview_runs).await;
|
||||
|
||||
let outcome = hooks.run_interrupt(request).await;
|
||||
emit_hook_completed_events(sess, turn_context, outcome.hook_events).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn run_pre_compact_hooks(
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
@@ -841,6 +871,7 @@ fn hook_run_metric_tags(run: &HookRunSummary) -> [(&'static str, &'static str);
|
||||
HookEventName::SubagentStart => "SubagentStart",
|
||||
HookEventName::SubagentStop => "SubagentStop",
|
||||
HookEventName::Stop => "Stop",
|
||||
HookEventName::Interrupt => "Interrupt",
|
||||
};
|
||||
let hook_source = match run.source {
|
||||
HookSource::System => "system",
|
||||
|
||||
@@ -26,6 +26,7 @@ use tracing::warn;
|
||||
use crate::codex_thread::BackgroundTerminalInfo;
|
||||
use crate::config::Config;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::hook_runtime::run_turn_interrupt_hooks;
|
||||
use crate::session::TurnInput;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn::run_hooks_and_record_inputs;
|
||||
@@ -791,6 +792,9 @@ impl Session {
|
||||
ThreadIdleCause::Completed
|
||||
};
|
||||
let event = if let Some(reason) = abort_reason {
|
||||
if reason == TurnAbortReason::Interrupted {
|
||||
run_turn_interrupt_hooks(self, &turn_context).await;
|
||||
}
|
||||
self.emit_turn_abort_lifecycle(reason.clone(), turn_context.extension_data.as_ref())
|
||||
.await;
|
||||
EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
@@ -941,6 +945,10 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
if reason == TurnAbortReason::Interrupted {
|
||||
run_turn_interrupt_hooks(self, &task.turn_context).await;
|
||||
}
|
||||
|
||||
let started_at = task
|
||||
.turn_context
|
||||
.turn_timing_state
|
||||
|
||||
Reference in New Issue
Block a user