Move Guardian reporting and denial accounting into the extension (#44544)

## What changed

- Move assessment event construction, metrics, and analytics tracking into `codex-guardian-reviewer` through `ReviewReport`.
- Store denial accounting in thread extension data through `ReviewDenials`, with core retaining turn interruption and lifecycle cleanup.
- Move failed-review record selection and bounded serialization into the extension, with core supplying captured review context.

## Testing

Add coverage that denial accounting clears on turn completion and interruption. Move the oversized-record test into the extension and exercise the new API.

GitOrigin-RevId: 6e5b3d9d4128b356f99b1d0e74a68111e40261ab
This commit is contained in:
jif
2026-09-10 14:13:57 +00:00
committed by copyberry
parent eca63f0803
commit 4e6d5c0a96
17 changed files with 455 additions and 296 deletions
@@ -1324,3 +1324,48 @@ 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
);
}