Manage synchronous Guardian reviewers through the thread manager (#43570)

## What changed

- Store the shared `GuardianReviewSessionManager` in thread extension data and let the Guardian extension spawn and fork reviewers through `ThreadManager`, independently of async scorer startup.
- Preserve reviewer reuse, concurrent forks, cancellation, inherited environments, and user instructions. Gate managed reviewer startup on parent registration and remove terminated reviewers from the manager.
- Retain the `guardian` subagent identity in saved conversations. Reject resume of live Guardian reviewers and reject client archive or delete of live owner-managed workers with JSON-RPC error `-32600`; allow normal access after the owner releases them.
- Keep standalone reviewer support for inline delegates and remove duplicated extension prompt and configuration code.

## Testing

Add app-server tests covering reviewer reuse and concurrent forks, inherited instructions and prompt cache keys, live resume and removal rejection, saved reviewer discovery and resume after parent shutdown, and strict Guardian assessment inside an inline review.

GitOrigin-RevId: 4f9893c1ade151eacb4c11ffeae3d9e9ec019187
This commit is contained in:
jif authored and copyberry committed 2026-09-07 19:43:10 +00:00
1 parent ca6fb194b6
commit 98a5cb46b1
33 files changed
+687 -1522

No files matched your search

+2 -2
View File
@@ -338,7 +338,7 @@ pub async fn thread_rollback(sess: &Arc<Session>, sub_id: String, num_turns: u32
sess.services
.thread_extension_data
.remove::<NodeReplReviewEvidence>();
sess.guardian_review_session.invalidate().await;
sess.guardian_review_session().invalidate().await;
sess.services
.agent_control
.rollout_budget()
@@ -426,7 +426,7 @@ pub(super) async fn shutdown_session_runtime(sess: &Arc<Session>) {
sess.mcp_refresh.close();
sess.services.mcp_runtime.shutdown().await;
}
sess.guardian_review_session.shutdown().await;
sess.guardian_review_session().shutdown().await;
crate::hook_runtime::run_session_end_hooks(sess).await;
}
+7
View File
@@ -2084,6 +2084,13 @@ impl Session {
));
}
/// Uses the extension-owned reviewer, or the same manager for standalone hosts.
pub(crate) fn guardian_review_session(&self) -> Arc<GuardianReviewSessionManager> {
self.services
.thread_extension_data
.get_or_init(GuardianReviewSessionManager::default)
}
/// Persist the event to rollout and send it to clients.
pub(crate) async fn send_event(&self, turn_context: &TurnContext, msg: EventMsg) {
let legacy_source = msg.clone();
-2
View File
@@ -71,7 +71,6 @@ pub(crate) struct Session {
pub(crate) active_turn: Mutex<Option<ActiveTurn>>,
pub(crate) async_hook_results: async_channel::Receiver<HookCompletedEvent>,
pub(crate) input_queue: InputQueue,
pub(crate) guardian_review_session: GuardianReviewSessionManager,
pub(crate) services: SessionServices,
pub(super) git_enrichment_policy: GitEnrichmentPolicy,
pub(super) fork_persistence: ForkPersistence,
@@ -1525,7 +1524,6 @@ impl Session {
active_turn: Mutex::new(None),
async_hook_results,
input_queue: InputQueue::new(),
guardian_review_session: GuardianReviewSessionManager::default(),
services,
git_enrichment_policy,
fork_persistence,
+4 -6
View File
@@ -6636,7 +6636,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
active_turn: Mutex::new(None),
async_hook_results,
input_queue: super::input_queue::InputQueue::new(),
guardian_review_session: crate::guardian::GuardianReviewSessionManager::default(),
services,
git_enrichment_policy: GitEnrichmentPolicy::Fresh,
fork_persistence: ForkPersistence::Copied,
@@ -8574,7 +8573,7 @@ async fn shutdown_and_wait_shuts_down_cached_guardian_subagent() {
session_loop_termination: session_loop_termination_from_handle(child_session_loop_handle),
};
parent_session
.guardian_review_session
.guardian_review_session()
.cache_for_test(child_session, child_io)
.await;
@@ -8606,13 +8605,13 @@ async fn cached_guardian_subagent_exposes_its_rollout_path() {
session_loop_termination: session_loop_termination_from_handle(child_session_loop_handle),
};
parent_session
.guardian_review_session
.guardian_review_session()
.cache_for_test(child_session, child_io)
.await;
assert_eq!(
parent_session
.guardian_review_session
.guardian_review_session()
.trunk_rollout_path()
.await,
Some(child_rollout_path)
@@ -8659,7 +8658,7 @@ async fn shutdown_and_wait_shuts_down_tracked_ephemeral_guardian_review() {
session_loop_termination: session_loop_termination_from_handle(child_session_loop_handle),
};
parent_session
.guardian_review_session
.guardian_review_session()
.register_ephemeral_for_test(child_session, child_io)
.await;
@@ -8943,7 +8942,6 @@ where
active_turn: Mutex::new(None),
async_hook_results,
input_queue: super::input_queue::InputQueue::new(),
guardian_review_session: crate::guardian::GuardianReviewSessionManager::default(),
services,
git_enrichment_policy: GitEnrichmentPolicy::Fresh,
fork_persistence: ForkPersistence::Copied,