mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
[codex] Preserve reviewer when resuming threads (#30278)
## Why A thread resumed without an explicit reviewer could pick up the reviewer from the current config instead of preserving the reviewer already in use by the thread. After an app restart, this meant a thread running with auto review could silently switch back to user review, and the next turn could continue under the wrong reviewer. ## What changed Persist the effective reviewer with each turn and restore the latest persisted value when the thread resumes. If the resume request explicitly provides a reviewer, that value still takes precedence. ## Test plan - Added a regression test that starts a thread with auto review, records a turn, restarts with user review in config, resumes without an override, and verifies that auto review is preserved. - `just test -p codex-protocol` - `just test -p codex-state` - `just test -p codex-rollout` - `just test -p codex-app-server thread_resume_preserves_persisted_approvals_reviewer` - Clippy for the affected crates
This commit is contained in:
@@ -206,6 +206,7 @@ struct ExecRunArgs {
|
||||
state_db: Option<StateDbHandle>,
|
||||
command: Option<ExecCommand>,
|
||||
config: Config,
|
||||
resume_approvals_reviewer_override: Option<codex_app_server_protocol::ApprovalsReviewer>,
|
||||
dangerously_bypass_approvals_and_sandbox: bool,
|
||||
exec_span: tracing::Span,
|
||||
images: Vec<PathBuf>,
|
||||
@@ -463,6 +464,10 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
build_config,
|
||||
)
|
||||
.await?;
|
||||
let resume_approvals_reviewer_override = cli_kv_overrides
|
||||
.iter()
|
||||
.any(|(key, _)| key == "approvals_reviewer")
|
||||
.then(|| config.approvals_reviewer.into());
|
||||
|
||||
#[allow(clippy::print_stderr)]
|
||||
match check_execpolicy_for_warnings(&config.config_layer_stack).await {
|
||||
@@ -576,6 +581,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
state_db,
|
||||
command,
|
||||
config,
|
||||
resume_approvals_reviewer_override,
|
||||
dangerously_bypass_approvals_and_sandbox,
|
||||
exec_span: exec_span.clone(),
|
||||
images,
|
||||
@@ -673,6 +679,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
state_db,
|
||||
command,
|
||||
config,
|
||||
resume_approvals_reviewer_override,
|
||||
dangerously_bypass_approvals_and_sandbox,
|
||||
exec_span,
|
||||
images,
|
||||
@@ -804,7 +811,11 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
&client,
|
||||
ClientRequest::ThreadResume {
|
||||
request_id: request_ids.next(),
|
||||
params: thread_resume_params_from_config(&config, thread_id),
|
||||
params: thread_resume_params_from_config(
|
||||
&config,
|
||||
thread_id,
|
||||
resume_approvals_reviewer_override,
|
||||
),
|
||||
},
|
||||
"thread/resume",
|
||||
)
|
||||
@@ -1066,7 +1077,7 @@ fn thread_start_params_from_config(config: &Config) -> ThreadStartParams {
|
||||
cwd: Some(config.cwd.to_string_lossy().to_string()),
|
||||
runtime_workspace_roots: Some(config.workspace_roots.clone()),
|
||||
approval_policy: Some(config.permissions.approval_policy.value().into()),
|
||||
approvals_reviewer: approvals_reviewer_override_from_config(config),
|
||||
approvals_reviewer: Some(config.approvals_reviewer.into()),
|
||||
sandbox: sandbox.flatten(),
|
||||
permissions,
|
||||
config: thread_config_overrides_from_config(config),
|
||||
@@ -1076,7 +1087,11 @@ fn thread_start_params_from_config(config: &Config) -> ThreadStartParams {
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_resume_params_from_config(config: &Config, thread_id: String) -> ThreadResumeParams {
|
||||
fn thread_resume_params_from_config(
|
||||
config: &Config,
|
||||
thread_id: String,
|
||||
approvals_reviewer_override: Option<codex_app_server_protocol::ApprovalsReviewer>,
|
||||
) -> ThreadResumeParams {
|
||||
let permissions = permissions_selection_from_config(config);
|
||||
let sandbox = permissions.is_none().then(|| {
|
||||
sandbox_mode_from_permission_profile(
|
||||
@@ -1091,7 +1106,7 @@ fn thread_resume_params_from_config(config: &Config, thread_id: String) -> Threa
|
||||
cwd: Some(config.cwd.to_string_lossy().to_string()),
|
||||
runtime_workspace_roots: Some(config.workspace_roots.clone()),
|
||||
approval_policy: Some(config.permissions.approval_policy.value().into()),
|
||||
approvals_reviewer: approvals_reviewer_override_from_config(config),
|
||||
approvals_reviewer: approvals_reviewer_override,
|
||||
sandbox: sandbox.flatten(),
|
||||
permissions,
|
||||
config: thread_config_overrides_from_config(config),
|
||||
@@ -1141,12 +1156,6 @@ fn sandbox_mode_from_permission_profile(
|
||||
}
|
||||
}
|
||||
|
||||
fn approvals_reviewer_override_from_config(
|
||||
config: &Config,
|
||||
) -> Option<codex_app_server_protocol::ApprovalsReviewer> {
|
||||
Some(config.approvals_reviewer.into())
|
||||
}
|
||||
|
||||
async fn send_request_with_response<T>(
|
||||
client: &InProcessAppServerClient,
|
||||
request: ClientRequest,
|
||||
|
||||
@@ -491,6 +491,39 @@ async fn thread_start_params_include_review_policy_when_auto_review_is_enabled()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_resume_params_only_include_explicit_review_policy_override() {
|
||||
let codex_home = tempdir().expect("create temp codex home");
|
||||
let cwd = tempdir().expect("create temp cwd");
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
|
||||
..Default::default()
|
||||
})
|
||||
.fallback_cwd(Some(cwd.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("build config with guardian review policy");
|
||||
|
||||
let params_without_override = thread_resume_params_from_config(
|
||||
&config,
|
||||
"thread-id".to_string(),
|
||||
/*approvals_reviewer_override*/ None,
|
||||
);
|
||||
let params_with_override = thread_resume_params_from_config(
|
||||
&config,
|
||||
"thread-id".to_string(),
|
||||
Some(codex_app_server_protocol::ApprovalsReviewer::AutoReview),
|
||||
);
|
||||
|
||||
assert_eq!(params_without_override.approvals_reviewer, None);
|
||||
assert_eq!(
|
||||
params_with_override.approvals_reviewer,
|
||||
Some(codex_app_server_protocol::ApprovalsReviewer::AutoReview)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_exec_config_retries_without_invalid_headless_policy_for_auto_review() {
|
||||
let codex_home = tempdir().expect("create temp codex home");
|
||||
@@ -616,7 +649,11 @@ async fn thread_lifecycle_params_preserve_hook_trust_bypass() {
|
||||
)]));
|
||||
|
||||
let start_params = thread_start_params_from_config(&config);
|
||||
let resume_params = thread_resume_params_from_config(&config, "thread-id".to_string());
|
||||
let resume_params = thread_resume_params_from_config(
|
||||
&config,
|
||||
"thread-id".to_string(),
|
||||
/*approvals_reviewer_override*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(start_params.config, expected_config);
|
||||
assert_eq!(resume_params.config, expected_config);
|
||||
@@ -648,7 +685,11 @@ async fn thread_lifecycle_params_include_legacy_sandbox_when_no_active_profile()
|
||||
.expect("build config with legacy sandbox override");
|
||||
|
||||
let start_params = thread_start_params_from_config(&config);
|
||||
let resume_params = thread_resume_params_from_config(&config, "thread-id".to_string());
|
||||
let resume_params = thread_resume_params_from_config(
|
||||
&config,
|
||||
"thread-id".to_string(),
|
||||
/*approvals_reviewer_override*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(config.permissions.active_permission_profile(), None);
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user