Add Guardian internal session support (#39994)

## What changed

- Add an extension API for spawning host-owned internal sessions and a
  `ThreadReadyInput` lifecycle callback that runs after thread registration.
- Add Guardian reviewer session scaffolding that records the parent thread and
  effective model, plus the under-development `guardian_ext` feature flag.
- Start internal sessions with fresh history while preserving parent lineage,
  shared session controls, and internal-thread visibility rules.
- Scope internal-session prompt cache keys to their source and parent thread,
  and expose `guardian` as an internal session source.

## Testing

- Cover internal-session spawning, parent metadata, history isolation, prompt
  cache keys, and extension spawner argument forwarding.

GitOrigin-RevId: 682dae80397d62bb36247796b447042e760ca364
This commit is contained in:
Won Park authored and copyberry committed 2026-08-21 19:11:05 +00:00
1 parent 79b7606803
commit 56012fafb8
22 files changed
+402 -10

No files matched your search

+81
View File
@@ -874,6 +874,87 @@ async fn start_thread_keeps_internal_threads_hidden_from_normal_lookups() {
assert!(manager.list_thread_ids().await.is_empty());
}
#[tokio::test]
async fn spawn_internal_session_preserves_parent_lineage_without_forking_history() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
let manager = ThreadManager::with_models_provider_and_home_for_tests(
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.to_path_buf(),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
);
let parent = manager
.start_thread(StartThreadOptions {
metrics_service_name: Some("codex_work_desktop".to_string()),
..StartThreadOptions::new(config.clone())
})
.await
.expect("start parent thread");
let reviewer = manager
.spawn_internal_session(
parent.thread_id,
StartThreadOptions {
session_source: Some(SessionSource::Internal(InternalSessionSource::Guardian)),
initial_history: InitialHistory::Forked(vec![RolloutItem::ResponseItem(
user_msg("parent history must not be inherited").into(),
)]),
environments: Some(Vec::new()),
..StartThreadOptions::new(config)
},
)
.await
.expect("start internal reviewer");
let reviewer_config = reviewer.thread.config_snapshot().await;
assert_eq!(
reviewer.session_configured.session_id,
parent.session_configured.session_id
);
assert!(std::ptr::eq(
reviewer
.thread
.session
.services
.agent_control
.rollout_budget(),
parent
.thread
.session
.services
.agent_control
.rollout_budget(),
));
assert_eq!(reviewer_config.parent_thread_id, Some(parent.thread_id));
assert_eq!(reviewer_config.forked_from_thread_id, None);
assert_eq!(reviewer_config.originator, "codex_work_desktop");
assert_eq!(
reviewer.session_configured.parent_thread_id,
Some(parent.thread_id)
);
assert_eq!(reviewer.session_configured.forked_from_id, None);
assert_eq!(manager.list_thread_ids().await, vec![parent.thread_id]);
assert!(manager.get_thread(reviewer.thread_id).await.is_err());
assert!(
reviewer
.thread
.session
.clone_history()
.await
.raw_items()
.next()
.is_none()
);
manager
.shutdown_all_threads_bounded(Duration::from_secs(10))
.await;
}
#[tokio::test]
async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors() {
struct InitialDataRecorder {