mirror of
https://github.com/openai/codex.git
synced 2026-11-14 14:49:31 +00:00
Route Guardian reviewers through ThreadManager for inline parents (#45518)
## Why Inline delegates have no entry in the thread registry, so reviewer creation cannot depend on looking up the parent or waiting for its thread-ready notification. ## What changed - Capture parent identity, authentication, shared agent control, originator, and inherited instructions in `StartThreadOptions` so `ThreadManager` can start a child without a registered parent. - Route Guardian reviewer creation through this path, remove the standalone fallback and readiness gate, and require a Guardian extension host. - Install explicit reviewer hosts in unit tests and the Guardian reviewer extension in the integration test harness, using `ExtensionRegistry::to_builder()` to preserve existing contributors. ## Testing Extend the thread-manager regression test to remove the parent from the registry before starting a child, then verify inherited lineage, originator, session identity, and authentication, and exclusion from the public thread list. GitOrigin-RevId: 468ded6fdce7520cb39d55c1a884dcfa5aaea2d9
This commit is contained in:
1 parent
280c7e1e56
commit
7c73903be2
18 files changed
+318
-199
No files matched your search
Generated
+1
@@ -5498,6 +5498,7 @@ dependencies = [
|
|||||||
"codex-exec-server",
|
"codex-exec-server",
|
||||||
"codex-extension-api",
|
"codex-extension-api",
|
||||||
"codex-features",
|
"codex-features",
|
||||||
|
"codex-guardian-v2",
|
||||||
"codex-home",
|
"codex-home",
|
||||||
"codex-hooks",
|
"codex-hooks",
|
||||||
"codex-http-client",
|
"codex-http-client",
|
||||||
|
|||||||
@@ -81,11 +81,8 @@ pub(crate) async fn run_codex_thread_interactive(
|
|||||||
} else {
|
} else {
|
||||||
Arc::clone(&parent_session.services.extensions)
|
Arc::clone(&parent_session.services.extensions)
|
||||||
};
|
};
|
||||||
// Inline delegates never register with ThreadManager or receive on_thread_ready.
|
|
||||||
// Bind their standalone spawn path before inherited extensions run.
|
|
||||||
let mut thread_extension_init = codex_extension_api::ExtensionDataInit::default();
|
let mut thread_extension_init = codex_extension_api::ExtensionDataInit::default();
|
||||||
thread_extension_init.insert(isolation);
|
thread_extension_init.insert(isolation);
|
||||||
thread_extension_init.insert(crate::guardian::GuardianReviewSessionHost::default());
|
|
||||||
let (session, io) = Session::spawn(SessionSpawnArgs {
|
let (session, io) = Session::spawn(SessionSpawnArgs {
|
||||||
startup: None,
|
startup: None,
|
||||||
config,
|
config,
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ mod review;
|
|||||||
mod review_session;
|
mod review_session;
|
||||||
mod reviewer_config;
|
mod reviewer_config;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) mod test_host;
|
||||||
|
|
||||||
use codex_protocol::items::ModelInvocationContext;
|
use codex_protocol::items::ModelInvocationContext;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
//! The extension owns review policy and pooling; this module binds runtime operations
|
//! The extension owns review policy and pooling; this module binds runtime operations
|
||||||
//! to the captured parent action, environments, authorization and context snapshots.
|
//! to the captured parent action, environments, authorization and context snapshots.
|
||||||
|
|
||||||
#[path = "review_session_factory.rs"]
|
#[path = "review_session_setup.rs"]
|
||||||
mod factory;
|
mod setup;
|
||||||
pub(crate) use factory::prewarm_guardian_review_session;
|
pub(crate) use setup::prewarm_guardian_review_session;
|
||||||
pub(crate) use factory::run_guardian_review_session;
|
pub(crate) use setup::run_guardian_review_session;
|
||||||
|
|
||||||
#[path = "review_session_threads.rs"]
|
#[path = "review_session_threads.rs"]
|
||||||
mod managed_threads;
|
mod managed_threads;
|
||||||
@@ -55,7 +55,6 @@ use tokio_util::sync::CancellationToken;
|
|||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::agents_md_manager::SessionInstructions;
|
use crate::agents_md_manager::SessionInstructions;
|
||||||
use crate::codex_delegate::run_codex_thread_interactive;
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::config::Constrained;
|
use crate::config::Constrained;
|
||||||
use crate::config::ManagedFeatures;
|
use crate::config::ManagedFeatures;
|
||||||
@@ -70,7 +69,6 @@ use crate::image_preparation::ImagePreparationMode;
|
|||||||
use crate::image_preparation::ImageResizeNoticeMode;
|
use crate::image_preparation::ImageResizeNoticeMode;
|
||||||
use crate::image_preparation::prepare_response_items;
|
use crate::image_preparation::prepare_response_items;
|
||||||
use crate::image_preparation::unified_image_budget_enabled;
|
use crate::image_preparation::unified_image_budget_enabled;
|
||||||
use crate::session::GitEnrichmentPolicy;
|
|
||||||
use crate::session::SessionIo;
|
use crate::session::SessionIo;
|
||||||
use crate::session::session::Session;
|
use crate::session::session::Session;
|
||||||
use crate::session::turn_context::TurnContext;
|
use crate::session::turn_context::TurnContext;
|
||||||
@@ -141,12 +139,6 @@ impl GuardianReviewSessionHost {
|
|||||||
managed_threads: Some(managed_threads::ManagedReviewerThreads::new(manager)),
|
managed_threads: Some(managed_threads::ManagedReviewerThreads::new(manager)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_ready(&self) {
|
|
||||||
if let Some(threads) = &self.managed_threads {
|
|
||||||
threads.mark_ready();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) type GuardianReviewSessionManager =
|
pub(crate) type GuardianReviewSessionManager =
|
||||||
|
|||||||
+95
-79
@@ -7,7 +7,7 @@ use codex_guardian_reviewer::ReviewerRequest;
|
|||||||
use codex_guardian_reviewer::ReviewerSessionFactory;
|
use codex_guardian_reviewer::ReviewerSessionFactory;
|
||||||
use codex_guardian_reviewer::SessionDisposition;
|
use codex_guardian_reviewer::SessionDisposition;
|
||||||
|
|
||||||
pub(super) struct PreparedSession {
|
pub(super) struct PreparedGuardianContext {
|
||||||
parent: Arc<Session>,
|
parent: Arc<Session>,
|
||||||
context: GuardianReviewContext,
|
context: GuardianReviewContext,
|
||||||
config: Config,
|
config: Config,
|
||||||
@@ -17,7 +17,7 @@ pub(super) struct PreparedSession {
|
|||||||
host: Arc<GuardianReviewSessionHost>,
|
host: Arc<GuardianReviewSessionHost>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PreparedSession {
|
impl PreparedGuardianContext {
|
||||||
async fn prepare(
|
async fn prepare(
|
||||||
parent: Arc<Session>,
|
parent: Arc<Session>,
|
||||||
context: GuardianReviewContext,
|
context: GuardianReviewContext,
|
||||||
@@ -57,10 +57,8 @@ impl PreparedSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReviewerSessionFactory for PreparedSession {
|
impl PreparedGuardianContext {
|
||||||
type Session = GuardianReviewSession;
|
fn reuse_key(&self, previous: Option<&GuardianReviewSession>) -> GuardianReviewSessionReuseKey {
|
||||||
|
|
||||||
fn context(&self, previous: Option<&GuardianReviewSession>) -> GuardianReviewSessionReuseKey {
|
|
||||||
let mut key = self.key.clone();
|
let mut key = self.key.clone();
|
||||||
if self.context_policy != ReviewContextPolicy::ThreadOwned
|
if self.context_policy != ReviewContextPolicy::ThreadOwned
|
||||||
&& self.parent_compaction.is_none()
|
&& self.parent_compaction.is_none()
|
||||||
@@ -73,6 +71,70 @@ impl ReviewerSessionFactory for PreparedSession {
|
|||||||
key
|
key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns captured parent inputs; Guardian selects the agent's identity and lifecycle.
|
||||||
|
async fn thread_options(
|
||||||
|
&self,
|
||||||
|
initial_history: Option<InitialHistory>,
|
||||||
|
) -> crate::StartThreadOptions {
|
||||||
|
let initial_history = initial_history.or_else(|| {
|
||||||
|
self.parent_compaction
|
||||||
|
.clone()
|
||||||
|
.map(|item| InitialHistory::Forked(vec![RolloutItem::ResponseItem(item.into())]))
|
||||||
|
});
|
||||||
|
let mut config = self.config.clone();
|
||||||
|
config.model_provider.supports_websockets &= self
|
||||||
|
.parent
|
||||||
|
.services
|
||||||
|
.model_client
|
||||||
|
.responses_websocket_enabled();
|
||||||
|
crate::StartThreadOptions {
|
||||||
|
internal_parent: Some(crate::thread_manager::InternalSessionParent {
|
||||||
|
thread_id: self.parent.thread_id(),
|
||||||
|
auth_manager: Arc::clone(&self.parent.services.auth_manager),
|
||||||
|
agent_control: self.parent.services.agent_control.clone(),
|
||||||
|
originator: self.context.turn().originator.clone(),
|
||||||
|
inherited_instructions: Some(self.parent.inherited_instructions().await),
|
||||||
|
}),
|
||||||
|
initial_history: initial_history.unwrap_or(InitialHistory::New),
|
||||||
|
environments: Some(self.context.environments().to_selections()),
|
||||||
|
inherited_environments: Some(self.context.environments().clone()),
|
||||||
|
client_mcp_extensions: self.parent.services.client_mcp_extensions.clone(),
|
||||||
|
..crate::StartThreadOptions::new(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Binds context bookkeeping to an agent that Guardian has already started.
|
||||||
|
async fn bind_session(
|
||||||
|
&self,
|
||||||
|
session: Arc<Session>,
|
||||||
|
io: SessionIo,
|
||||||
|
context: GuardianReviewSessionReuseKey,
|
||||||
|
state: GuardianReviewState,
|
||||||
|
cancellation: CancellationToken,
|
||||||
|
) -> GuardianReviewSession {
|
||||||
|
let inherited = session.inherited_instructions().await;
|
||||||
|
let context = GuardianReviewSessionReuseKey {
|
||||||
|
user_instructions: inherited.user,
|
||||||
|
thread_instructions: inherited.thread,
|
||||||
|
..context
|
||||||
|
};
|
||||||
|
GuardianReviewSession {
|
||||||
|
session,
|
||||||
|
io,
|
||||||
|
cancel_token: cancellation,
|
||||||
|
reuse_key: context,
|
||||||
|
state: Mutex::new(state),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReviewerSessionFactory for PreparedGuardianContext {
|
||||||
|
type Session = GuardianReviewSession;
|
||||||
|
|
||||||
|
fn context(&self, previous: Option<&GuardianReviewSession>) -> GuardianReviewSessionReuseKey {
|
||||||
|
self.reuse_key(previous)
|
||||||
|
}
|
||||||
|
|
||||||
async fn spawn(
|
async fn spawn(
|
||||||
&self,
|
&self,
|
||||||
context: GuardianReviewSessionReuseKey,
|
context: GuardianReviewSessionReuseKey,
|
||||||
@@ -80,87 +142,41 @@ impl ReviewerSessionFactory for PreparedSession {
|
|||||||
snapshot: Option<GuardianReviewForkSnapshot>,
|
snapshot: Option<GuardianReviewForkSnapshot>,
|
||||||
cancellation: CancellationToken,
|
cancellation: CancellationToken,
|
||||||
) -> anyhow::Result<GuardianReviewSession> {
|
) -> anyhow::Result<GuardianReviewSession> {
|
||||||
let mut config = self.config.clone();
|
let (conversation, history) = snapshot.map(ConversationState::fork).unzip();
|
||||||
if matches!(kind, GuardianReviewSessionKind::EphemeralForked) {
|
let state = GuardianReviewState {
|
||||||
config.ephemeral = true;
|
conversation: conversation.unwrap_or_default(),
|
||||||
}
|
last_admitted_node_repl_response_sequence: history.as_ref().map_or(0, |history| {
|
||||||
let (initial_history, conversation, last_admitted_node_repl_response_sequence) =
|
history.last_admitted_node_repl_response_sequence
|
||||||
match snapshot {
|
|
||||||
Some(snapshot) => {
|
|
||||||
let (conversation, history) = ConversationState::fork(snapshot);
|
|
||||||
(
|
|
||||||
Some(history.initial_history),
|
|
||||||
conversation,
|
|
||||||
history.last_admitted_node_repl_response_sequence,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
None => (
|
|
||||||
self.parent_compaction.clone().map(|item| {
|
|
||||||
InitialHistory::Forked(vec![RolloutItem::ResponseItem(item.into())])
|
|
||||||
}),
|
|
||||||
ConversationState::default(),
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
let (session, io) = match &self.host.managed_threads {
|
|
||||||
Some(threads) => {
|
|
||||||
threads
|
|
||||||
.spawn(
|
|
||||||
&self.parent,
|
|
||||||
&self.context,
|
|
||||||
config,
|
|
||||||
cancellation.clone(),
|
|
||||||
initial_history,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
Box::pin(run_codex_thread_interactive(
|
|
||||||
config,
|
|
||||||
Arc::clone(&self.parent.services.auth_manager),
|
|
||||||
self.parent.services.models_manager.clone(),
|
|
||||||
Arc::clone(&self.parent),
|
|
||||||
Arc::clone(self.context.turn()),
|
|
||||||
self.context.environments().clone(),
|
|
||||||
cancellation.clone(),
|
|
||||||
SubAgentSource::Other(GUARDIAN_REVIEWER_NAME.to_owned()),
|
|
||||||
codex_extension_api::SessionIsolation::Isolated,
|
|
||||||
initial_history,
|
|
||||||
GitEnrichmentPolicy::Skip,
|
|
||||||
codex_sandboxing::WindowsSandboxProxySettingsMode::Preserve,
|
|
||||||
))
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let inherited = session.inherited_instructions().await;
|
|
||||||
let context = GuardianReviewSessionReuseKey {
|
|
||||||
user_instructions: inherited.user,
|
|
||||||
thread_instructions: inherited.thread,
|
|
||||||
..context
|
|
||||||
};
|
|
||||||
Ok(GuardianReviewSession {
|
|
||||||
session,
|
|
||||||
io,
|
|
||||||
cancel_token: cancellation,
|
|
||||||
reuse_key: context,
|
|
||||||
state: Mutex::new(GuardianReviewState {
|
|
||||||
conversation,
|
|
||||||
last_admitted_node_repl_response_sequence,
|
|
||||||
pending_node_repl_evidence_admission: None,
|
|
||||||
}),
|
}),
|
||||||
})
|
pending_node_repl_evidence_admission: None,
|
||||||
|
};
|
||||||
|
let mut options = self
|
||||||
|
.thread_options(history.map(|history| history.initial_history))
|
||||||
|
.await;
|
||||||
|
if matches!(kind, GuardianReviewSessionKind::EphemeralForked) {
|
||||||
|
options.config.ephemeral = true;
|
||||||
|
}
|
||||||
|
let threads = self.host.managed_threads.as_ref().ok_or_else(|| {
|
||||||
|
anyhow::anyhow!("Guardian extension is not installed for this thread")
|
||||||
|
})?;
|
||||||
|
let (session, io) = threads
|
||||||
|
.spawn(&self.parent, options, cancellation.clone())
|
||||||
|
.await?;
|
||||||
|
Ok(self
|
||||||
|
.bind_session(session, io, context, state, cancellation)
|
||||||
|
.await)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) struct PreparedReview {
|
pub(super) struct PreparedReview {
|
||||||
factory: PreparedSession,
|
factory: PreparedGuardianContext,
|
||||||
params: GuardianReviewSessionParams,
|
params: GuardianReviewSessionParams,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReviewerRequest for PreparedReview {
|
impl ReviewerRequest for PreparedReview {
|
||||||
type Factory = PreparedSession;
|
type Factory = PreparedGuardianContext;
|
||||||
|
|
||||||
fn factory(&self) -> &PreparedSession {
|
fn factory(&self) -> &PreparedGuardianContext {
|
||||||
&self.factory
|
&self.factory
|
||||||
}
|
}
|
||||||
fn deadline(&self) -> tokio::time::Instant {
|
fn deadline(&self) -> tokio::time::Instant {
|
||||||
@@ -212,7 +228,7 @@ pub(crate) async fn run_guardian_review_session(
|
|||||||
pub(super) async fn prepare_review(
|
pub(super) async fn prepare_review(
|
||||||
params: GuardianReviewSessionParams,
|
params: GuardianReviewSessionParams,
|
||||||
) -> anyhow::Result<PreparedReview> {
|
) -> anyhow::Result<PreparedReview> {
|
||||||
let factory = PreparedSession::prepare(
|
let factory = PreparedGuardianContext::prepare(
|
||||||
Arc::clone(¶ms.parent_session),
|
Arc::clone(¶ms.parent_session),
|
||||||
params.parent_context.clone(),
|
params.parent_context.clone(),
|
||||||
params.spawn_config.clone(),
|
params.spawn_config.clone(),
|
||||||
@@ -233,7 +249,7 @@ pub(crate) fn prewarm_guardian_review_session(
|
|||||||
let context = GuardianReviewContext::from(turn);
|
let context = GuardianReviewContext::from(turn);
|
||||||
let config = guardian_review_session_config(&parent, &context).await?;
|
let config = guardian_review_session_config(&parent, &context).await?;
|
||||||
let history = parent.clone_history().await;
|
let history = parent.clone_history().await;
|
||||||
let factory = PreparedSession::prepare(
|
let factory = PreparedGuardianContext::prepare(
|
||||||
Arc::clone(&parent),
|
Arc::clone(&parent),
|
||||||
context,
|
context,
|
||||||
config.spawn_config,
|
config.spawn_config,
|
||||||
@@ -76,7 +76,7 @@ async fn run_review_preserves_evidence_during_parent_compaction() {
|
|||||||
let manager = GuardianReviewSessionManager::default();
|
let manager = GuardianReviewSessionManager::default();
|
||||||
prewarm_test_session(&manager, reviewer).await;
|
prewarm_test_session(&manager, reviewer).await;
|
||||||
// Capture the review context, then compact the parent before the reviewer builds its prompt.
|
// Capture the review context, then compact the parent before the reviewer builds its prompt.
|
||||||
let prepared = factory::prepare_review(params).await.unwrap();
|
let prepared = setup::prepare_review(params).await.unwrap();
|
||||||
let checkpoint: ResponseItem = serde_json::from_value(serde_json::json!({
|
let checkpoint: ResponseItem = serde_json::from_value(serde_json::json!({
|
||||||
"type": "compaction", "id": "cmp_new", "encrypted_content": "new-checkpoint"
|
"type": "compaction", "id": "cmp_new", "encrypted_content": "new-checkpoint"
|
||||||
}))
|
}))
|
||||||
@@ -312,9 +312,7 @@ async fn spawned_guardian_reuse_key_matches_inherited_instructions() {
|
|||||||
thread_instructions: latest.clone(),
|
thread_instructions: latest.clone(),
|
||||||
..stale_key.clone()
|
..stale_key.clone()
|
||||||
};
|
};
|
||||||
let prepared = factory::prepare_review(params)
|
let prepared = setup::prepare_review(params).await.expect("prepare review");
|
||||||
.await
|
|
||||||
.expect("prepare review");
|
|
||||||
let review = prepared
|
let review = prepared
|
||||||
.factory()
|
.factory()
|
||||||
.spawn(
|
.spawn(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//! Adapts the existing reviewer lifecycle to the extension's ThreadManager.
|
//! Adapts the existing reviewer lifecycle to the extension's ThreadManager.
|
||||||
//! Parent registration gates spawning; prompts, reuse and review outcomes stay shared.
|
//! Captured parent inputs support inline delegates; prompts and review outcomes stay shared.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Weak;
|
use std::sync::Weak;
|
||||||
@@ -11,15 +11,12 @@ use codex_protocol::protocol::InternalSessionSource;
|
|||||||
use codex_protocol::protocol::SessionSource;
|
use codex_protocol::protocol::SessionSource;
|
||||||
use codex_protocol::protocol::SubAgentSource;
|
use codex_protocol::protocol::SubAgentSource;
|
||||||
use codex_protocol::protocol::ThreadSource;
|
use codex_protocol::protocol::ThreadSource;
|
||||||
use tokio::sync::watch;
|
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use super::GUARDIAN_REVIEWER_NAME;
|
use super::GUARDIAN_REVIEWER_NAME;
|
||||||
use super::GuardianReviewContext;
|
|
||||||
use crate::StartThreadOptions;
|
use crate::StartThreadOptions;
|
||||||
use crate::ThreadManager;
|
use crate::ThreadManager;
|
||||||
use crate::codex_delegate::forward_session_io;
|
use crate::codex_delegate::forward_session_io;
|
||||||
use crate::config::Config;
|
|
||||||
use crate::config::Constrained;
|
use crate::config::Constrained;
|
||||||
use crate::session::SessionIo;
|
use crate::session::SessionIo;
|
||||||
use crate::session::emit_subagent_session_started;
|
use crate::session::emit_subagent_session_started;
|
||||||
@@ -27,72 +24,41 @@ use crate::session::session::Session;
|
|||||||
|
|
||||||
pub(super) struct ManagedReviewerThreads {
|
pub(super) struct ManagedReviewerThreads {
|
||||||
manager: Weak<ThreadManager>,
|
manager: Weak<ThreadManager>,
|
||||||
ready: watch::Sender<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManagedReviewerThreads {
|
impl ManagedReviewerThreads {
|
||||||
pub(super) fn new(manager: Weak<ThreadManager>) -> Self {
|
pub(super) fn new(manager: Weak<ThreadManager>) -> Self {
|
||||||
Self {
|
Self { manager }
|
||||||
manager,
|
|
||||||
ready: watch::channel(/*init*/ false).0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn mark_ready(&self) {
|
|
||||||
self.ready.send_replace(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn spawn(
|
pub(super) async fn spawn(
|
||||||
&self,
|
&self,
|
||||||
parent: &Arc<Session>,
|
parent: &Arc<Session>,
|
||||||
context: &GuardianReviewContext,
|
mut options: StartThreadOptions,
|
||||||
mut config: Config,
|
|
||||||
cancel: CancellationToken,
|
cancel: CancellationToken,
|
||||||
history: Option<InitialHistory>,
|
|
||||||
) -> anyhow::Result<(Arc<Session>, SessionIo)> {
|
) -> anyhow::Result<(Arc<Session>, SessionIo)> {
|
||||||
let mut ready = self.ready.subscribe();
|
|
||||||
tokio::select! {
|
|
||||||
result = ready.wait_for(|ready| *ready) => { result?; }
|
|
||||||
_ = cancel.cancelled() => anyhow::bail!("guardian reviewer is shutting down"),
|
|
||||||
}
|
|
||||||
let manager = self
|
let manager = self
|
||||||
.manager
|
.manager
|
||||||
.upgrade()
|
.upgrade()
|
||||||
.ok_or_else(|| anyhow::anyhow!("thread manager is no longer available"))?;
|
.ok_or_else(|| anyhow::anyhow!("thread manager is no longer available"))?;
|
||||||
// Match the standalone delegate's settings before using the managed spawn path.
|
if options.config.permissions.approval_policy.value() != AskForApproval::Never {
|
||||||
if config.permissions.approval_policy.value() != AskForApproval::Never {
|
|
||||||
anyhow::bail!("Codex delegates require approval policy `never`");
|
anyhow::bail!("Codex delegates require approval policy `never`");
|
||||||
}
|
}
|
||||||
config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
|
options.config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
|
||||||
config.model_provider.supports_websockets &=
|
options.session_source = Some(SessionSource::Internal(InternalSessionSource::Guardian));
|
||||||
parent.services.model_client.responses_websocket_enabled();
|
options.thread_source = Some(ThreadSource::GuardianReview);
|
||||||
let mut thread_extension_init = codex_extension_api::ExtensionDataInit::default();
|
options
|
||||||
thread_extension_init.insert(codex_extension_api::SessionIsolation::Isolated);
|
.thread_extension_init
|
||||||
let options = StartThreadOptions {
|
.insert(codex_extension_api::SessionIsolation::Isolated);
|
||||||
thread_extension_init,
|
options.initial_history = match options.initial_history {
|
||||||
session_source: Some(SessionSource::Internal(InternalSessionSource::Guardian)),
|
|
||||||
thread_source: Some(ThreadSource::GuardianReview),
|
|
||||||
environments: Some(context.environments().to_selections()),
|
|
||||||
inherited_environments: Some(context.environments().clone()),
|
|
||||||
client_mcp_extensions: parent.services.client_mcp_extensions.clone(),
|
|
||||||
..StartThreadOptions::new(config)
|
|
||||||
};
|
|
||||||
let history = match history.unwrap_or(InitialHistory::New) {
|
|
||||||
InitialHistory::Forked(history) => InitialHistory::Forked(history),
|
InitialHistory::Forked(history) => InitialHistory::Forked(history),
|
||||||
InitialHistory::New | InitialHistory::Cleared => InitialHistory::New,
|
InitialHistory::New | InitialHistory::Cleared => InitialHistory::New,
|
||||||
InitialHistory::Resumed(_) => {
|
InitialHistory::Resumed(_) => {
|
||||||
anyhow::bail!("guardian review forks cannot resume an existing thread")
|
anyhow::bail!("guardian review forks cannot resume an existing thread")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Generic internal sessions start isolated. Reviewers explicitly inherit
|
|
||||||
// the parent's applied instructions, just like standalone delegates.
|
|
||||||
let spawned = manager
|
let spawned = manager
|
||||||
.spawn_internal_session_with_history(
|
.start_thread(options)
|
||||||
parent.thread_id(),
|
|
||||||
options,
|
|
||||||
history,
|
|
||||||
Some(parent.inherited_instructions().await),
|
|
||||||
)
|
|
||||||
.or_cancel(&cancel)
|
.or_cancel(&cancel)
|
||||||
.await
|
.await
|
||||||
.map_err(codex_protocol::error::CodexErr::from)??;
|
.map_err(codex_protocol::error::CodexErr::from)??;
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//! Explicit ThreadManager host for core's context-adapter unit tests.
|
||||||
|
//! Application-level tests install the real Guardian extension instead.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use codex_home::CodexHomeUserInstructionsProvider;
|
||||||
|
use codex_protocol::protocol::SessionSource;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::session::session::Session;
|
||||||
|
|
||||||
|
pub(crate) fn install(session: &Session, config: &Config) {
|
||||||
|
let manager = Arc::new(crate::ThreadManager::new(
|
||||||
|
config,
|
||||||
|
Arc::clone(&session.services.auth_manager),
|
||||||
|
Arc::clone(&session.services.models_manager),
|
||||||
|
crate::CodexAppsToolsCache::default(),
|
||||||
|
SessionSource::Exec,
|
||||||
|
session.services.turn_environments.environment_manager(),
|
||||||
|
codex_extension_api::empty_extension_registry(),
|
||||||
|
Arc::new(CodexHomeUserInstructionsProvider::new(
|
||||||
|
config.codex_home.clone(),
|
||||||
|
)),
|
||||||
|
/*analytics_events_client*/ None,
|
||||||
|
crate::passthrough_image_store(),
|
||||||
|
Arc::clone(&session.services.thread_store),
|
||||||
|
/*agent_graph_store*/ None,
|
||||||
|
session.installation_id.clone(),
|
||||||
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
|
));
|
||||||
|
let host = super::GuardianReviewSessionHost::with_thread_manager(Arc::downgrade(&manager));
|
||||||
|
session.services.thread_extension_data.insert(host);
|
||||||
|
session.services.thread_extension_data.insert(manager);
|
||||||
|
}
|
||||||
@@ -178,6 +178,7 @@ async fn guardian_test_session_turn_and_rx(
|
|||||||
.expect("session should be uniquely owned")
|
.expect("session should be uniquely owned")
|
||||||
.services
|
.services
|
||||||
.models_manager = models_manager;
|
.models_manager = models_manager;
|
||||||
|
crate::guardian::test_host::install(&session, &config);
|
||||||
let turn_mut = Arc::get_mut(&mut turn).expect("turn should be uniquely owned");
|
let turn_mut = Arc::get_mut(&mut turn).expect("turn should be uniquely owned");
|
||||||
turn_mut.config = Arc::clone(&config);
|
turn_mut.config = Arc::clone(&config);
|
||||||
turn_mut.provider =
|
turn_mut.provider =
|
||||||
@@ -240,6 +241,7 @@ async fn guardian_test_session_and_turn_with_base_url(
|
|||||||
config.model_provider.clone(),
|
config.model_provider.clone(),
|
||||||
);
|
);
|
||||||
session.services.models_manager = models_manager;
|
session.services.models_manager = models_manager;
|
||||||
|
crate::guardian::test_host::install(&session, &config);
|
||||||
turn.config = Arc::clone(&config);
|
turn.config = Arc::clone(&config);
|
||||||
turn.provider = create_model_provider(config.model_provider.clone(), turn.auth_manager.clone());
|
turn.provider = create_model_provider(config.model_provider.clone(), turn.auth_manager.clone());
|
||||||
|
|
||||||
@@ -1973,6 +1975,7 @@ async fn guardian_request_model_for_auto_review(
|
|||||||
.expect("session should be unique")
|
.expect("session should be unique")
|
||||||
.services
|
.services
|
||||||
.models_manager = Arc::new(models_manager);
|
.models_manager = Arc::new(models_manager);
|
||||||
|
crate::guardian::test_host::install(&session, &turn.config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
update_turn_settings_for_test(
|
update_turn_settings_for_test(
|
||||||
@@ -2188,6 +2191,7 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot()
|
|||||||
config.model_provider.clone(),
|
config.model_provider.clone(),
|
||||||
);
|
);
|
||||||
session.services.models_manager = models_manager;
|
session.services.models_manager = models_manager;
|
||||||
|
crate::guardian::test_host::install(&session, &config);
|
||||||
let memory_extension = Arc::new(GuardianMemoryContextProbe);
|
let memory_extension = Arc::new(GuardianMemoryContextProbe);
|
||||||
let mut extensions = codex_extension_api::ExtensionRegistryBuilder::<Config>::new();
|
let mut extensions = codex_extension_api::ExtensionRegistryBuilder::<Config>::new();
|
||||||
extensions.thread_lifecycle_contributor(memory_extension.clone());
|
extensions.thread_lifecycle_contributor(memory_extension.clone());
|
||||||
@@ -3018,6 +3022,7 @@ async fn guardian_review_surfaces_responses_api_errors_in_rejection_reason() ->
|
|||||||
.expect("session should be uniquely owned")
|
.expect("session should be uniquely owned")
|
||||||
.services
|
.services
|
||||||
.models_manager = models_manager;
|
.models_manager = models_manager;
|
||||||
|
crate::guardian::test_host::install(&session, &config);
|
||||||
let turn_mut = Arc::get_mut(&mut turn).expect("turn should be uniquely owned");
|
let turn_mut = Arc::get_mut(&mut turn).expect("turn should be uniquely owned");
|
||||||
turn_mut.config = Arc::clone(&config);
|
turn_mut.config = Arc::clone(&config);
|
||||||
turn_mut.provider =
|
turn_mut.provider =
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ mod thread_manager;
|
|||||||
pub(crate) mod web_search;
|
pub(crate) mod web_search;
|
||||||
pub(crate) mod windows_sandbox_read_grants;
|
pub(crate) mod windows_sandbox_read_grants;
|
||||||
pub use thread_manager::ForkSnapshot;
|
pub use thread_manager::ForkSnapshot;
|
||||||
|
pub use thread_manager::InternalSessionParent;
|
||||||
pub use thread_manager::NewThread;
|
pub use thread_manager::NewThread;
|
||||||
pub use thread_manager::StartThreadOptions;
|
pub use thread_manager::StartThreadOptions;
|
||||||
pub use thread_manager::ThreadManager;
|
pub use thread_manager::ThreadManager;
|
||||||
|
|||||||
@@ -6167,6 +6167,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
|||||||
skills_snapshot,
|
skills_snapshot,
|
||||||
);
|
);
|
||||||
session.mark_mcp_runtime_dirty();
|
session.mark_mcp_runtime_dirty();
|
||||||
|
crate::guardian::test_host::install(&session, &turn_context.config);
|
||||||
(session, turn_context)
|
(session, turn_context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8344,6 +8345,7 @@ where
|
|||||||
skills_snapshot,
|
skills_snapshot,
|
||||||
));
|
));
|
||||||
session.mark_mcp_runtime_dirty();
|
session.mark_mcp_runtime_dirty();
|
||||||
|
crate::guardian::test_host::install(&session, &turn_context.config);
|
||||||
(session, turn_context, rx_event)
|
(session, turn_context, rx_event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ async fn request_permissions_routes_to_guardian_when_reviewer_is_enabled() {
|
|||||||
config.model_provider.clone(),
|
config.model_provider.clone(),
|
||||||
);
|
);
|
||||||
session.services.models_manager = models_manager;
|
session.services.models_manager = models_manager;
|
||||||
|
crate::guardian::test_host::install(&session, &config);
|
||||||
turn_context_raw.config = Arc::clone(&config);
|
turn_context_raw.config = Arc::clone(&config);
|
||||||
turn_context_raw.provider = create_model_provider(
|
turn_context_raw.provider = create_model_provider(
|
||||||
config.model_provider.clone(),
|
config.model_provider.clone(),
|
||||||
@@ -423,6 +424,7 @@ async fn request_permissions_guardian_review_stops_when_cancelled() {
|
|||||||
.expect("single session ref")
|
.expect("single session ref")
|
||||||
.services
|
.services
|
||||||
.models_manager = models_manager;
|
.models_manager = models_manager;
|
||||||
|
crate::guardian::test_host::install(&session, &config);
|
||||||
turn_context_raw.config = Arc::clone(&config);
|
turn_context_raw.config = Arc::clone(&config);
|
||||||
turn_context_raw.provider = create_model_provider(
|
turn_context_raw.provider = create_model_provider(
|
||||||
config.model_provider.clone(),
|
config.model_provider.clone(),
|
||||||
@@ -549,6 +551,7 @@ async fn guardian_allows_exec_command_additional_permissions_requests_past_polic
|
|||||||
config.model_provider.clone(),
|
config.model_provider.clone(),
|
||||||
);
|
);
|
||||||
session.services.models_manager = models_manager;
|
session.services.models_manager = models_manager;
|
||||||
|
crate::guardian::test_host::install(&session, &config);
|
||||||
turn_context_raw.config = Arc::clone(&config);
|
turn_context_raw.config = Arc::clone(&config);
|
||||||
turn_context_raw.provider = create_model_provider(
|
turn_context_raw.provider = create_model_provider(
|
||||||
config.model_provider.clone(),
|
config.model_provider.clone(),
|
||||||
@@ -668,6 +671,7 @@ async fn strict_auto_review_turn_grant_forces_guardian_for_exec_command_policy_s
|
|||||||
config.model_provider.clone(),
|
config.model_provider.clone(),
|
||||||
);
|
);
|
||||||
session.services.models_manager = models_manager;
|
session.services.models_manager = models_manager;
|
||||||
|
crate::guardian::test_host::install(&session, &config);
|
||||||
turn_context_raw.config = Arc::clone(&config);
|
turn_context_raw.config = Arc::clone(&config);
|
||||||
turn_context_raw.provider = create_model_provider(
|
turn_context_raw.provider = create_model_provider(
|
||||||
config.model_provider.clone(),
|
config.model_provider.clone(),
|
||||||
|
|||||||
@@ -232,6 +232,18 @@ pub struct ThreadManager {
|
|||||||
_test_codex_home_guard: Option<TempCodexHomeGuard>,
|
_test_codex_home_guard: Option<TempCodexHomeGuard>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Captured parent identity for an internal child, including inline parents that
|
||||||
|
/// do not have a public entry in the thread manager. Authentication and the shared
|
||||||
|
/// agent budget remain bound to the parent that created this value.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct InternalSessionParent {
|
||||||
|
pub(crate) thread_id: ThreadId,
|
||||||
|
pub(crate) auth_manager: Arc<AuthManager>,
|
||||||
|
pub(crate) agent_control: AgentControl,
|
||||||
|
pub(crate) originator: String,
|
||||||
|
pub(crate) inherited_instructions: Option<SessionInstructions>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct StartThreadOptions {
|
pub struct StartThreadOptions {
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
/// Host-owned provider for this root thread's additional instructions.
|
/// Host-owned provider for this root thread's additional instructions.
|
||||||
@@ -249,6 +261,8 @@ pub struct StartThreadOptions {
|
|||||||
pub thread_instructions_provider: Option<Arc<dyn ThreadInstructionsProvider>>,
|
pub thread_instructions_provider: Option<Arc<dyn ThreadInstructionsProvider>>,
|
||||||
pub allow_provider_model_fallback: bool,
|
pub allow_provider_model_fallback: bool,
|
||||||
pub initial_history: InitialHistory,
|
pub initial_history: InitialHistory,
|
||||||
|
/// Parent captured for `start_thread`, including a parent not in the live registry.
|
||||||
|
pub internal_parent: Option<InternalSessionParent>,
|
||||||
pub history_mode: Option<ThreadHistoryMode>,
|
pub history_mode: Option<ThreadHistoryMode>,
|
||||||
pub session_source: Option<SessionSource>,
|
pub session_source: Option<SessionSource>,
|
||||||
pub thread_source: Option<ThreadSource>,
|
pub thread_source: Option<ThreadSource>,
|
||||||
@@ -275,6 +289,7 @@ impl StartThreadOptions {
|
|||||||
thread_instructions_provider: None,
|
thread_instructions_provider: None,
|
||||||
allow_provider_model_fallback: false,
|
allow_provider_model_fallback: false,
|
||||||
initial_history: InitialHistory::New,
|
initial_history: InitialHistory::New,
|
||||||
|
internal_parent: None,
|
||||||
history_mode: None,
|
history_mode: None,
|
||||||
session_source: None,
|
session_source: None,
|
||||||
thread_source: None,
|
thread_source: None,
|
||||||
@@ -298,6 +313,7 @@ struct ThreadSpawnRequest {
|
|||||||
auth_manager: Arc<AuthManager>,
|
auth_manager: Arc<AuthManager>,
|
||||||
agent_control: AgentControl,
|
agent_control: AgentControl,
|
||||||
parent_thread_id: Option<ThreadId>,
|
parent_thread_id: Option<ThreadId>,
|
||||||
|
parent_originator: Option<String>,
|
||||||
forked_from_thread_id: Option<ThreadId>,
|
forked_from_thread_id: Option<ThreadId>,
|
||||||
fork_persistence: ForkPersistence,
|
fork_persistence: ForkPersistence,
|
||||||
inherited_environments: Option<TurnEnvironmentSnapshot>,
|
inherited_environments: Option<TurnEnvironmentSnapshot>,
|
||||||
@@ -318,6 +334,7 @@ impl ThreadSpawnRequest {
|
|||||||
auth_manager,
|
auth_manager,
|
||||||
agent_control,
|
agent_control,
|
||||||
parent_thread_id: None,
|
parent_thread_id: None,
|
||||||
|
parent_originator: None,
|
||||||
forked_from_thread_id: None,
|
forked_from_thread_id: None,
|
||||||
fork_persistence: ForkPersistence::Copied,
|
fork_persistence: ForkPersistence::Copied,
|
||||||
inherited_environments: None,
|
inherited_environments: None,
|
||||||
@@ -1044,17 +1061,15 @@ impl ThreadManager {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
let parent = self.get_thread(parent_thread_id).await?;
|
let parent = self.get_thread(parent_thread_id).await?;
|
||||||
let forked_from_thread_id = history.forked_from_id();
|
|
||||||
options.initial_history = history;
|
options.initial_history = history;
|
||||||
let mut request = ThreadSpawnRequest::new(
|
options.internal_parent = Some(InternalSessionParent {
|
||||||
options,
|
thread_id: parent_thread_id,
|
||||||
Arc::clone(&parent.session.services.auth_manager),
|
auth_manager: Arc::clone(&parent.session.services.auth_manager),
|
||||||
parent.session.services.agent_control.clone(),
|
agent_control: parent.session.services.agent_control.clone(),
|
||||||
);
|
originator: parent.config_snapshot().await.originator,
|
||||||
request.parent_thread_id = Some(parent_thread_id);
|
inherited_instructions,
|
||||||
request.forked_from_thread_id = forked_from_thread_id;
|
});
|
||||||
request.inherited_instructions = inherited_instructions;
|
self.start_thread(options).await
|
||||||
Box::pin(self.state.spawn_thread(request)).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allocates a thread ID before startup so a caller can associate host-owned state with it.
|
/// Allocates a thread ID before startup so a caller can associate host-owned state with it.
|
||||||
@@ -1068,7 +1083,6 @@ impl ThreadManager {
|
|||||||
forked_from_thread_id: Option<ThreadId>,
|
forked_from_thread_id: Option<ThreadId>,
|
||||||
startup: Option<Arc<crate::session::startup::SessionStartup>>,
|
startup: Option<Arc<crate::session::startup::SessionStartup>>,
|
||||||
) -> CodexResult<NewThread> {
|
) -> CodexResult<NewThread> {
|
||||||
let agent_control = self.agent_control_for_config(&options.config);
|
|
||||||
let (resumed_session_source, resumed_thread_source) = options
|
let (resumed_session_source, resumed_thread_source) = options
|
||||||
.initial_history
|
.initial_history
|
||||||
.get_resumed_session_sources()
|
.get_resumed_session_sources()
|
||||||
@@ -1080,9 +1094,33 @@ impl ThreadManager {
|
|||||||
.unwrap_or(resumed_session_source),
|
.unwrap_or(resumed_session_source),
|
||||||
);
|
);
|
||||||
options.thread_source = options.thread_source.take().or(resumed_thread_source);
|
options.thread_source = options.thread_source.take().or(resumed_thread_source);
|
||||||
let mut request =
|
let parent = options.internal_parent.take();
|
||||||
ThreadSpawnRequest::new(options, Arc::clone(&self.state.auth_manager), agent_control);
|
if parent.is_some()
|
||||||
request.forked_from_thread_id = forked_from_thread_id;
|
&& (!matches!(options.session_source, Some(SessionSource::Internal(_)))
|
||||||
|
|| matches!(options.initial_history, InitialHistory::Resumed(_)))
|
||||||
|
{
|
||||||
|
return Err(CodexErr::InvalidRequest(
|
||||||
|
"a captured internal parent requires a new or forked internal session".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut request = if let Some(parent) = parent {
|
||||||
|
let mut request =
|
||||||
|
ThreadSpawnRequest::new(options, parent.auth_manager, parent.agent_control);
|
||||||
|
request.parent_thread_id = Some(parent.thread_id);
|
||||||
|
request.parent_originator = Some(parent.originator);
|
||||||
|
request.inherited_instructions = parent.inherited_instructions;
|
||||||
|
request.forked_from_thread_id = request.options.initial_history.forked_from_id();
|
||||||
|
request
|
||||||
|
} else {
|
||||||
|
let agent_control = self.agent_control_for_config(&options.config);
|
||||||
|
let mut request = ThreadSpawnRequest::new(
|
||||||
|
options,
|
||||||
|
Arc::clone(&self.state.auth_manager),
|
||||||
|
agent_control,
|
||||||
|
);
|
||||||
|
request.forked_from_thread_id = forked_from_thread_id;
|
||||||
|
request
|
||||||
|
};
|
||||||
request.startup = startup;
|
request.startup = startup;
|
||||||
Box::pin(self.state.spawn_thread(request)).await
|
Box::pin(self.state.spawn_thread(request)).await
|
||||||
}
|
}
|
||||||
@@ -1748,6 +1786,7 @@ impl ThreadManagerState {
|
|||||||
session_source: &SessionSource,
|
session_source: &SessionSource,
|
||||||
parent_thread_id: Option<ThreadId>,
|
parent_thread_id: Option<ThreadId>,
|
||||||
forked_from_thread_id: Option<ThreadId>,
|
forked_from_thread_id: Option<ThreadId>,
|
||||||
|
parent_originator: Option<String>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let persisted_originator = initial_history.get_session_originator();
|
let persisted_originator = initial_history.get_session_originator();
|
||||||
let inherited_originator = match initial_history {
|
let inherited_originator = match initial_history {
|
||||||
@@ -1777,7 +1816,7 @@ impl ThreadManagerState {
|
|||||||
metrics_service_name,
|
metrics_service_name,
|
||||||
env_originator,
|
env_originator,
|
||||||
persisted_originator,
|
persisted_originator,
|
||||||
inherited_originator,
|
inherited_originator.or(parent_originator),
|
||||||
originator().value,
|
originator().value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1938,6 +1977,7 @@ impl ThreadManagerState {
|
|||||||
auth_manager,
|
auth_manager,
|
||||||
agent_control,
|
agent_control,
|
||||||
parent_thread_id,
|
parent_thread_id,
|
||||||
|
parent_originator,
|
||||||
forked_from_thread_id,
|
forked_from_thread_id,
|
||||||
fork_persistence,
|
fork_persistence,
|
||||||
inherited_environments,
|
inherited_environments,
|
||||||
@@ -1950,6 +1990,7 @@ impl ThreadManagerState {
|
|||||||
thread_instructions_provider,
|
thread_instructions_provider,
|
||||||
allow_provider_model_fallback,
|
allow_provider_model_fallback,
|
||||||
initial_history,
|
initial_history,
|
||||||
|
internal_parent: _,
|
||||||
history_mode,
|
history_mode,
|
||||||
session_source,
|
session_source,
|
||||||
thread_source,
|
thread_source,
|
||||||
@@ -2084,6 +2125,7 @@ impl ThreadManagerState {
|
|||||||
&session_source,
|
&session_source,
|
||||||
parent_thread_id,
|
parent_thread_id,
|
||||||
forked_from_thread_id,
|
forked_from_thread_id,
|
||||||
|
parent_originator,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let source_changed_during_startup = Arc::new(AtomicBool::new(false));
|
let source_changed_during_startup = Arc::new(AtomicBool::new(false));
|
||||||
|
|||||||
@@ -1476,6 +1476,47 @@ async fn spawn_internal_session_preserves_parent_lineage_without_forking_history
|
|||||||
.is_none()
|
.is_none()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Captured lineage also works for an inline parent with no registry entry.
|
||||||
|
manager.remove_thread(&parent.thread_id).await;
|
||||||
|
let child = manager
|
||||||
|
.start_thread(StartThreadOptions {
|
||||||
|
internal_parent: Some(InternalSessionParent {
|
||||||
|
thread_id: parent.thread_id,
|
||||||
|
auth_manager: Arc::clone(&parent.thread.session.services.auth_manager),
|
||||||
|
agent_control: parent.thread.session.services.agent_control.clone(),
|
||||||
|
originator: reviewer_config.originator.clone(),
|
||||||
|
inherited_instructions: None,
|
||||||
|
}),
|
||||||
|
session_source: Some(SessionSource::Internal(
|
||||||
|
InternalSessionSource::MemoryConsolidation,
|
||||||
|
)),
|
||||||
|
..StartThreadOptions::new(parent.thread.session.get_config().await.as_ref().clone())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("start child without a parent registry entry");
|
||||||
|
let child_config = child.thread.config_snapshot().await;
|
||||||
|
assert_eq!(
|
||||||
|
(
|
||||||
|
child_config.parent_thread_id,
|
||||||
|
child_config.originator,
|
||||||
|
child.thread.session.session_id(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Some(parent.thread_id),
|
||||||
|
reviewer_config.originator,
|
||||||
|
parent.thread.session.session_id(),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert!(Arc::ptr_eq(
|
||||||
|
&child.thread.session.services.auth_manager,
|
||||||
|
&parent.thread.session.services.auth_manager
|
||||||
|
));
|
||||||
|
assert!(manager.list_thread_ids().await.is_empty());
|
||||||
|
parent
|
||||||
|
.thread
|
||||||
|
.shutdown_and_wait()
|
||||||
|
.await
|
||||||
|
.expect("stop parent");
|
||||||
manager
|
manager
|
||||||
.shutdown_all_threads_bounded(Duration::from_secs(10))
|
.shutdown_all_threads_bounded(Duration::from_secs(10))
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ codex-config = { workspace = true }
|
|||||||
codex-core = { workspace = true }
|
codex-core = { workspace = true }
|
||||||
codex-extension-api = { workspace = true }
|
codex-extension-api = { workspace = true }
|
||||||
codex-exec-server = { workspace = true }
|
codex-exec-server = { workspace = true }
|
||||||
|
codex-guardian-v2 = { workspace = true }
|
||||||
codex-home = { workspace = true }
|
codex-home = { workspace = true }
|
||||||
codex-features = { workspace = true }
|
codex-features = { workspace = true }
|
||||||
codex-hooks = { workspace = true }
|
codex-hooks = { workspace = true }
|
||||||
|
|||||||
@@ -719,39 +719,45 @@ impl TestCodexBuilder {
|
|||||||
.models_manager
|
.models_manager
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| codex_core::build_models_manager(&config, auth_manager.clone()));
|
.unwrap_or_else(|| codex_core::build_models_manager(&config, auth_manager.clone()));
|
||||||
let thread_manager = ThreadManager::new(
|
|
||||||
&config,
|
|
||||||
auth_manager.clone(),
|
|
||||||
models_manager,
|
|
||||||
codex_core::CodexAppsToolsCache::default(),
|
|
||||||
SessionSource::Exec,
|
|
||||||
Arc::clone(&environment_manager),
|
|
||||||
Arc::clone(&self.extensions),
|
|
||||||
user_instructions_provider,
|
|
||||||
/*analytics_events_client*/ None,
|
|
||||||
codex_core::passthrough_image_store(),
|
|
||||||
Arc::clone(&thread_store),
|
|
||||||
codex_core::local_agent_graph_store_from_state_db(state_db.as_ref()),
|
|
||||||
installation_id,
|
|
||||||
/*attestation_provider*/ None,
|
|
||||||
/*external_time_provider*/ self.external_time_provider.clone(),
|
|
||||||
);
|
|
||||||
let code_mode_host_program = self
|
let code_mode_host_program = self
|
||||||
.code_mode_host_program
|
.code_mode_host_program
|
||||||
.take()
|
.take()
|
||||||
.or_else(|| codex_utils_cargo_bin::cargo_bin("codex-code-mode-host").ok());
|
.or_else(|| codex_utils_cargo_bin::cargo_bin("codex-code-mode-host").ok());
|
||||||
let thread_manager = if config.features.enabled(Feature::CodeModeHost)
|
let thread_manager = Arc::new_cyclic(|manager| {
|
||||||
&& let Some(code_mode_host_program) = code_mode_host_program
|
let mut extensions = self.extensions.to_builder();
|
||||||
{
|
let guardian = Arc::new(codex_guardian_v2::GuardianReviewerExtension::new(
|
||||||
codex_core::test_support::with_code_mode_host_program(
|
manager.clone(),
|
||||||
thread_manager,
|
));
|
||||||
code_mode_host_program,
|
extensions.thread_lifecycle_contributor(guardian);
|
||||||
|
let thread_manager = ThreadManager::new(
|
||||||
&config,
|
&config,
|
||||||
)
|
auth_manager.clone(),
|
||||||
} else {
|
models_manager,
|
||||||
thread_manager
|
codex_core::CodexAppsToolsCache::default(),
|
||||||
};
|
SessionSource::Exec,
|
||||||
let thread_manager = Arc::new(thread_manager);
|
Arc::clone(&environment_manager),
|
||||||
|
Arc::new(extensions.build()),
|
||||||
|
user_instructions_provider,
|
||||||
|
/*analytics_events_client*/ None,
|
||||||
|
codex_core::passthrough_image_store(),
|
||||||
|
Arc::clone(&thread_store),
|
||||||
|
codex_core::local_agent_graph_store_from_state_db(state_db.as_ref()),
|
||||||
|
installation_id,
|
||||||
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ self.external_time_provider.clone(),
|
||||||
|
);
|
||||||
|
if config.features.enabled(Feature::CodeModeHost)
|
||||||
|
&& let Some(code_mode_host_program) = code_mode_host_program
|
||||||
|
{
|
||||||
|
codex_core::test_support::with_code_mode_host_program(
|
||||||
|
thread_manager,
|
||||||
|
code_mode_host_program,
|
||||||
|
&config,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
thread_manager
|
||||||
|
}
|
||||||
|
});
|
||||||
let user_shell_override = self.user_shell_override.clone();
|
let user_shell_override = self.user_shell_override.clone();
|
||||||
let client_mcp_extensions = || {
|
let client_mcp_extensions = || {
|
||||||
ClientMcpExtensions::new(
|
ClientMcpExtensions::new(
|
||||||
|
|||||||
@@ -162,6 +162,28 @@ pub struct ExtensionRegistry<C: Sync> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Sync> ExtensionRegistry<C> {
|
impl<C: Sync> ExtensionRegistry<C> {
|
||||||
|
/// Copies the registered contributors into a builder for host-specific additions.
|
||||||
|
pub fn to_builder(&self) -> ExtensionRegistryBuilder<C> {
|
||||||
|
ExtensionRegistryBuilder {
|
||||||
|
registry: Self {
|
||||||
|
event_sink: self.event_sink.clone(),
|
||||||
|
turn_start_admission: self.turn_start_admission.clone(),
|
||||||
|
thread_lifecycle_contributors: self.thread_lifecycle_contributors.clone(),
|
||||||
|
turn_lifecycle_contributors: self.turn_lifecycle_contributors.clone(),
|
||||||
|
config_contributors: self.config_contributors.clone(),
|
||||||
|
token_usage_contributors: self.token_usage_contributors.clone(),
|
||||||
|
skill_invocation_contributors: self.skill_invocation_contributors.clone(),
|
||||||
|
context_contributors: self.context_contributors.clone(),
|
||||||
|
mcp_server_contributors: self.mcp_server_contributors.clone(),
|
||||||
|
turn_input_contributors: self.turn_input_contributors.clone(),
|
||||||
|
tool_contributors: self.tool_contributors.clone(),
|
||||||
|
tool_lifecycle_contributors: self.tool_lifecycle_contributors.clone(),
|
||||||
|
turn_item_contributors: self.turn_item_contributors.clone(),
|
||||||
|
approval_review_contributors: self.approval_review_contributors.clone(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Acquires the host's turn-start permit, or an empty permit for ungated hosts.
|
/// Acquires the host's turn-start permit, or an empty permit for ungated hosts.
|
||||||
/// A missing permit rejects the start before Core consumes pending input.
|
/// A missing permit rejects the start before Core consumes pending input.
|
||||||
pub fn admit_turn_start(&self) -> Option<Box<dyn Send>> {
|
pub fn admit_turn_start(&self) -> Option<Box<dyn Send>> {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use codex_core::guardian_review::GuardianReviewSessionHost;
|
|||||||
use codex_extension_api::ExtensionFuture;
|
use codex_extension_api::ExtensionFuture;
|
||||||
use codex_extension_api::ExtensionRegistryBuilder;
|
use codex_extension_api::ExtensionRegistryBuilder;
|
||||||
use codex_extension_api::ThreadLifecycleContributor;
|
use codex_extension_api::ThreadLifecycleContributor;
|
||||||
use codex_extension_api::ThreadReadyInput;
|
|
||||||
use codex_extension_api::ThreadStartInput;
|
use codex_extension_api::ThreadStartInput;
|
||||||
|
|
||||||
/// Owns reviewer state through the same thread manager as the parent conversation.
|
/// Owns reviewer state through the same thread manager as the parent conversation.
|
||||||
@@ -39,17 +38,6 @@ impl ThreadLifecycleContributor<Config> for GuardianExtension {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_thread_ready<'a>(
|
|
||||||
&'a self,
|
|
||||||
input: ThreadReadyInput<'a, Config>,
|
|
||||||
) -> ExtensionFuture<'a, ()> {
|
|
||||||
Box::pin(async move {
|
|
||||||
if let Some(sessions) = input.thread_store.get::<GuardianReviewSessionHost>() {
|
|
||||||
sessions.mark_ready();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn install(
|
pub fn install(
|
||||||
|
|||||||
Reference in new issue
Block a user