diff --git a/codex-rs/app-server/src/request_processors/background_agent_live.rs b/codex-rs/app-server/src/request_processors/background_agent_live.rs index be01f7a3f..15c46c81b 100644 --- a/codex-rs/app-server/src/request_processors/background_agent_live.rs +++ b/codex-rs/app-server/src/request_processors/background_agent_live.rs @@ -4208,10 +4208,7 @@ async fn resolve_background_agent_config( }) .and_then(Value::as_str) .map(str::to_string); - let service_tier = payload - .and_then(|payload| payload.get("serviceTier")) - .and_then(Value::as_str) - .map(|value| Some(value.to_string())); + let service_tier = background_agent_snapshot_service_tier(payload)?; let approval_policy = payload .and_then(|payload| payload.get("approvalPolicy")) .cloned() @@ -4319,6 +4316,28 @@ async fn resolve_background_agent_config( }) } +fn background_agent_snapshot_service_tier( + payload: Option<&serde_json::Map>, +) -> anyhow::Result>> { + let Some(payload) = payload else { + return Ok(None); + }; + let Some(service_tier) = payload.get("serviceTier") else { + return Ok(None); + }; + // Workflow admission snapshots always include this field, using null to + // represent the absence of a requested tier. Preserve that absence so the + // loaded config cannot invent the explicit default tier and diverge from + // the immutable admission receipt. Other snapshots retain explicit null + // as the ConfigOverrides reset-to-default signal. + if service_tier.is_null() && payload.contains_key("routeReceipt") { + return Ok(None); + } + serde_json::from_value::>(service_tier.clone()) + .map(Some) + .map_err(anyhow::Error::from) +} + fn validate_background_agent_initial_execution_snapshot( run: &BackgroundAgentRun, snapshot: &BackgroundAgentExecutionSnapshot, @@ -6451,6 +6470,39 @@ mod tests { ); } + #[test] + fn background_agent_snapshot_service_tier_preserves_explicit_null() { + assert_eq!( + background_agent_snapshot_service_tier(/*payload*/ None).unwrap(), + None + ); + + let explicit_null_payload = json!({ + "serviceTier": null, + }); + assert_eq!( + background_agent_snapshot_service_tier(explicit_null_payload.as_object()).unwrap(), + Some(None) + ); + + let explicit_value_payload = json!({ + "serviceTier": "default", + }); + assert_eq!( + background_agent_snapshot_service_tier(explicit_value_payload.as_object()).unwrap(), + Some(Some("default".to_string())) + ); + + let workflow_null_payload = json!({ + "serviceTier": null, + "routeReceipt": {}, + }); + assert_eq!( + background_agent_snapshot_service_tier(workflow_null_payload.as_object()).unwrap(), + None + ); + } + #[test] fn launch_capability_requests_merge_server_defaults_and_reject_caller_duplicates() { let mut requests = vec![ diff --git a/codex-rs/state/src/runtime/workflow_orchestrator.rs b/codex-rs/state/src/runtime/workflow_orchestrator.rs index 6739206f4..7aff71fa4 100644 --- a/codex-rs/state/src/runtime/workflow_orchestrator.rs +++ b/codex-rs/state/src/runtime/workflow_orchestrator.rs @@ -929,7 +929,6 @@ async fn admit_ready_workflow_branches_in_tx( candidate: &candidate, attempt: branch_attempt, route_receipt: &route_receipt, - workspace_json: workspace_json.as_ref(), provisioned_workspace: &provisioned_workspace, review_context: review_context.as_ref(), background_agent_run_id: background_agent_run_id.as_str(), @@ -1340,7 +1339,6 @@ struct BackgroundBranchRunCreate<'a> { candidate: &'a ReadyBranchCandidate, attempt: i64, route_receipt: &'a WorkflowRouteReceipt, - workspace_json: Option<&'a Value>, provisioned_workspace: &'a ProvisionedWorkflowWorkspace, review_context: Option<&'a WorkflowReviewBranchContext>, background_agent_run_id: &'a str, @@ -1725,7 +1723,6 @@ async fn create_background_branch_run_if_missing_in_tx( candidate, attempt, route_receipt, - workspace_json: _, provisioned_workspace, review_context: _, background_agent_run_id, @@ -2267,6 +2264,10 @@ fn branch_execution_payload( let workspace_root = crate::runtime::managed_worktrees::path_to_db_string( provisioned_workspace.worktree_path.as_path(), ); + let workspace_mode = match provisioned_workspace.mode { + WorkflowWorkspaceMode::IsolatedWorktree => "isolated_worktree", + WorkflowWorkspaceMode::SharedRepository => "shared_repository", + }; json!({ "snapshotSource": "workflow/branch_admission", "workflowRunId": run.run_id.as_str(), @@ -2293,7 +2294,7 @@ fn branch_execution_payload( .auth_profile .as_deref() .map(|profile| StateRuntime::background_agent_identity_sha256(profile.as_bytes())), - "workspace": branch.workspace_json, + "workspace": json!({"mode": workspace_mode}), "workflowBootstrap": &candidate.bootstrap_json, "reviewContext": branch.review_context.map(|context| &context.state_json), "envSnapshotPolicy": "inherit-minimal", @@ -6913,6 +6914,15 @@ WHERE worktree_id = ? .await .expect("execution snapshot should load") .expect("execution snapshot should exist"); + assert_eq!( + Some("shared_repository"), + execution_snapshot + .payload_json + .get("workspace") + .and_then(|workspace| workspace.get("mode")) + .and_then(Value::as_str), + "shared non-Git branch must preserve the effective shared-repository workspace mode" + ); assert_eq!( Some( crate::runtime::managed_worktrees::path_to_db_string(source_cwd.as_path()).as_str()