diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_policy_v2.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_policy_v2.rs index 90fcf28f1..58edceff3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_policy_v2.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_policy_v2.rs @@ -839,13 +839,24 @@ fn create_v2_immutable_file(root: &Path, relative: &str, bytes: &[u8]) -> Result .open(&path) { Ok(mut file) => { - harden_new_game_creator_private_path(&path, false, "Planning V2 文件")?; - file.write_all(bytes) - .and_then(|_| file.sync_data()) - .map_err(|error| { - format!("写入 Planning V2 文件失败:{}: {error}", path.display()) - })?; - Ok(()) + // `create_new` has already made the final path visible. Every + // subsequent step must therefore be transactional from the + // caller's perspective: a failed ACL repair, partial write, or + // failed sync must not leave an empty/truncated immutable file + // that would permanently block a retry. + let result = (|| { + harden_new_game_creator_private_path(&path, false, "Planning V2 文件")?; + file.write_all(bytes) + .and_then(|_| file.sync_data()) + .map_err(|error| { + format!("写入 Planning V2 文件失败:{}: {error}", path.display()) + })?; + Ok(()) + })(); + if result.is_err() { + let _ = fs::remove_file(&path); + } + result } Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => { prepare_game_creator_private_path_for_read(&path, false, "Planning V2 文件")?;