Consolidate Guardian reviewer lifecycle ownership (#45672)

## What changed

- Use cancellation guards to tie reusable reviewers and temporary forks to their lifetimes, including when a review future is dropped. Replace cancelled reusable reviewers before reuse.
- Share `ReviewerTasks` between the pool and `ThreadManager`, and wait for tracked cleanup during pool shutdown. Remove the separate session shutdown protocol.
- Move denial cleanup from core task handling into Guardian's turn start, stop, and abort hooks. Expose `install_reviewer` to register both thread and turn lifecycle contributors together.

## Testing

Add an integration test that exercises stale denial cleanup at turn start, completion, and interruption, including starting another turn after interruption.

GitOrigin-RevId: 85badb235309c1e1576547d1746f7ee9fc66d545
This commit is contained in:
jif
2026-09-15 10:45:16 +00:00
committed by copyberry
parent 4415f985dc
commit a113f3e063
15 changed files with 231 additions and 315 deletions
@@ -1330,48 +1330,3 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
);
drop(io);
}
#[test_case(TerminalEventKind::TurnComplete; "completion")]
#[test_case(TerminalEventKind::TurnAborted; "interruption")]
#[tokio::test]
async fn terminal_turn_clears_extension_owned_denials(terminal: TerminalEventKind) {
let (session, turn, events) = make_session_and_context_with_rx().await;
let finish = Arc::new(tokio::sync::Notify::new());
session
.spawn_task(
Arc::clone(&turn),
Vec::new(),
HeldStepTask {
kind: TaskKind::Regular,
finish: Arc::clone(&finish),
},
)
.await;
let denials =
codex_guardian_reviewer::ReviewDenials::for_thread(&session.services.thread_extension_data);
for _ in 0..2 {
assert_eq!(
denials.record_denial(&turn.sub_id, turn.model_info()).await,
None
);
}
match terminal {
TerminalEventKind::TurnComplete => finish.notify_one(),
TerminalEventKind::TurnAborted => {
session.abort_all_tasks(TurnAbortReason::Interrupted).await
}
}
recv_terminal_event(&events, terminal).await;
// Delivery precedes accounting cleanup. Wait for the runtime to finish the turn.
timeout(Duration::from_secs(/*secs*/ 5), async {
while session.active_turn.lock().await.is_some() {
tokio::task::yield_now().await;
}
})
.await
.expect("turn becomes idle");
assert_eq!(
denials.record_denial(&turn.sub_id, turn.model_info()).await,
None
);
}