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:
Andrei Eternal
2026-08-25 01:27:18 +00:00
committed by copyberry
parent 9b2ef38f54
commit cbfd999db7
49 changed files with 1393 additions and 48 deletions
@@ -779,6 +779,7 @@ fn event_label(event_name: HookEventName) -> &'static str {
HookEventName::SubagentStart => "SubagentStart",
HookEventName::SubagentStop => "SubagentStop",
HookEventName::Stop => "Stop",
HookEventName::Interrupt => "Interrupt",
}
}
@@ -795,6 +796,7 @@ fn event_description(event_name: HookEventName) -> &'static str {
HookEventName::SubagentStart => "When a subagent is created",
HookEventName::SubagentStop => "Right before a subagent ends its turn",
HookEventName::Stop => "Right before Codex ends its turn",
HookEventName::Interrupt => "Right before an interrupted turn is aborted",
}
}
@@ -18,5 +18,6 @@ expression: "render_lines(&view, 112)"
SubagentStart 0 0 When a subagent is created
SubagentStop 0 0 Right before a subagent ends its turn
Stop 0 0 Right before Codex ends its turn
Interrupt 0 0 Right before an interrupted turn is aborted
Press enter to view hooks; esc to close
@@ -22,5 +22,6 @@ expression: "render_lines(&view, 112)"
SubagentStart 0 0 When a subagent is created
SubagentStop 0 0 Right before a subagent ends its turn
Stop 0 0 Right before Codex ends its turn
Interrupt 0 0 Right before an interrupted turn is aborted
Press enter to view hooks; esc to close
@@ -20,5 +20,6 @@ expression: "render_lines(&view, 112)"
SubagentStart 0 0 0 When a subagent is created
SubagentStop 0 0 0 Right before a subagent ends its turn
Stop 0 0 0 Right before Codex ends its turn
Interrupt 0 0 0 Right before an interrupted turn is aborted
Press t to trust all; enter to review hooks; esc to close
@@ -21,5 +21,6 @@ expression: popup
SubagentStart 0 0 When a subagent is created
SubagentStop 0 0 Right before a subagent ends its turn
Stop 0 0 Right before Codex ends its turn
Interrupt 0 0 Right before an interrupted turn is aborted
Press enter to view hooks; esc to close
@@ -0,0 +1,5 @@
---
source: tui/src/chatwidget/tests/helpers.rs
expression: combined
---
• Interrupt (completed) says: Heads up from the hook
+12 -10
View File
@@ -1670,6 +1670,16 @@ pub(super) async fn assert_hook_events_snapshot(
"hook start should render in the live hook cell"
);
let mut entries = vec![codex_app_server_protocol::HookOutputEntry {
kind: codex_app_server_protocol::HookOutputEntryKind::Warning,
text: "Heads up from the hook".to_string(),
}];
if event_name != codex_app_server_protocol::HookEventName::Interrupt {
entries.push(codex_app_server_protocol::HookOutputEntry {
kind: codex_app_server_protocol::HookOutputEntryKind::Context,
text: "Remember the startup checklist.".to_string(),
});
}
handle_hook_completed(
&mut chat,
hook_run(
@@ -1677,16 +1687,7 @@ pub(super) async fn assert_hook_events_snapshot(
event_name,
codex_app_server_protocol::HookRunStatus::Completed,
status_message,
vec![
codex_app_server_protocol::HookOutputEntry {
kind: codex_app_server_protocol::HookOutputEntryKind::Warning,
text: "Heads up from the hook".to_string(),
},
codex_app_server_protocol::HookOutputEntry {
kind: codex_app_server_protocol::HookOutputEntryKind::Context,
text: "Remember the startup checklist.".to_string(),
},
],
entries,
),
);
@@ -1711,5 +1712,6 @@ fn hook_event_label(event_name: codex_app_server_protocol::HookEventName) -> &'s
codex_app_server_protocol::HookEventName::SubagentStart => "SubagentStart",
codex_app_server_protocol::HookEventName::SubagentStop => "SubagentStop",
codex_app_server_protocol::HookEventName::Stop => "Stop",
codex_app_server_protocol::HookEventName::Interrupt => "Interrupt",
}
}
@@ -4810,6 +4810,17 @@ async fn user_prompt_submit_app_server_hook_notifications_render_snapshot() {
assert!(!chat.bottom_pane.status_indicator_visible());
}
#[tokio::test]
async fn interrupt_hook_events_render_snapshot() {
assert_hook_events_snapshot(
codex_app_server_protocol::HookEventName::Interrupt,
"interrupt:0:/tmp/hooks.json",
"cleaning up the interrupted turn",
"interrupt_hook_events_render_snapshot",
)
.await;
}
#[tokio::test]
async fn pre_tool_use_hook_events_render_snapshot() {
assert_hook_events_snapshot(
@@ -846,6 +846,7 @@ fn hook_event_label(event_name: HookEventName) -> &'static str {
HookEventName::SubagentStart => "SubagentStart",
HookEventName::SubagentStop => "SubagentStop",
HookEventName::Stop => "Stop",
HookEventName::Interrupt => "Interrupt",
}
}