Run executor hooks for interrupted turns (#41432)

## Why

Executor-scoped cleanup hooks need to observe an interrupted turn as well as a
normally completed one.

## What changed

- Allow bundled executor plugins to register the allowlisted `Interrupt` hook
  alongside `Stop`, while preserving multiple allowed hook entries from the
  selected executor environment.
- Run interrupt hooks with the interrupted turn's last step discovery and turn
  metadata. Turns without a step context do not reuse discovery from an earlier
  turn.
- Keep executor-scoped interrupt hooks asynchronous and omit public hook
  lifecycle events for them, matching executor `Stop` hook behavior.

## Testing

Added coverage for hook filtering and registration, interrupt execution and
metadata propagation, environment selection, and turns without a step context.

GitOrigin-RevId: f0c536b3382a37cf3bf693c4573d21f6f66ace4e
This commit is contained in:
Krish Chainani authored and copyberry committed 2026-08-28 23:03:05 +00:00
1 parent 0d22692962
commit c2abf869d5
9 files changed
+473 -165

No files matched your search

+14 -5
View File
@@ -12,6 +12,7 @@ use codex_diagnostics::Gauge;
use codex_extension_api::ThreadIdleCause;
use futures::future::BoxFuture;
use tokio::select;
use tokio::sync::Mutex;
use tokio::sync::Notify;
use tokio_util::sync::CancellationToken;
use tokio_util::task::AbortOnDropHandle;
@@ -35,6 +36,7 @@ use crate::session::turn_context::TurnContext;
use crate::state::ActiveTurn;
use crate::state::RunningTask;
use crate::state::TaskKind;
use crate::state::TurnState;
use codex_analytics::TurnProfileFact;
use codex_analytics::TurnTokenUsageFact;
use codex_context_fragments::RenderedFragment;
@@ -518,7 +520,8 @@ impl Session {
aborted_turn = task.is_some();
turn_context = task.as_ref().map(|task| Arc::clone(&task.turn_context));
if let Some(task) = task {
self.handle_task_abort(task, reason.clone()).await;
self.handle_task_abort(task, reason.clone(), &active_turn.turn_state)
.await;
}
if aborted_turn {
active_turn_to_clear = Some(active_turn);
@@ -569,7 +572,8 @@ impl Session {
let task = active_turn.task.take();
let turn_context = task.as_ref().map(|task| Arc::clone(&task.turn_context));
if let Some(task) = task {
self.handle_task_abort(task, reason.clone()).await;
self.handle_task_abort(task, reason.clone(), &active_turn.turn_state)
.await;
}
if let Some(turn_context) = turn_context.as_deref() {
self.emit_turn_abort_lifecycle(reason.clone(), turn_context.extension_data.as_ref())
@@ -805,7 +809,7 @@ impl Session {
};
let event = if let Some(reason) = abort_reason {
if reason == TurnAbortReason::Interrupted {
run_turn_interrupt_hooks(self, &turn_context).await;
run_turn_interrupt_hooks(self, &turn_context, &turn_state).await;
}
self.emit_turn_abort_lifecycle(reason.clone(), turn_context.extension_data.as_ref())
.await;
@@ -898,7 +902,12 @@ impl Session {
.await
}
async fn handle_task_abort(self: &Arc<Self>, task: RunningTask, reason: TurnAbortReason) {
async fn handle_task_abort(
self: &Arc<Self>,
task: RunningTask,
reason: TurnAbortReason,
turn_state: &Mutex<TurnState>,
) {
let sub_id = task.turn_context.sub_id.clone();
if task.cancellation_token.is_cancelled() {
return;
@@ -958,7 +967,7 @@ impl Session {
}
if reason == TurnAbortReason::Interrupted {
run_turn_interrupt_hooks(self, &task.turn_context).await;
run_turn_interrupt_hooks(self, &task.turn_context, turn_state).await;
}
let started_at = task